@neocompose/cli 0.62.1 → 0.62.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.62.2] - 2026-10-02
4
+
5
+ - Speed up `neo test` after production edits by sharing one indexed compiler context across specs and omitting the push-only source serialization round trip. Production compilation and whole-graph validation still run.
6
+ - Execute spec files in parallel with up to four CPU workers by default. Configure `test.maxWorkers`, override it with `--maxWorkers <n>`, or use `--runInBand` for serial execution. Workers reuse the prepared project and retain per-test fixture and mock isolation.
7
+ - Report worker startup and elapsed execution separately from summed parallel test work.
8
+
3
9
  ## [0.62.1] - 2026-10-02
4
10
 
5
11
  - Construct populated unordered member defaults through initializers so fresh entries survive test, push, and pull. Keep nested list rows in their existing representation.
package/README.md CHANGED
@@ -418,12 +418,26 @@ JavaScript regular expression:
418
418
  neo test
419
419
  neo test Classes/Player.spec.neo
420
420
  neo test -t "TakeDamage"
421
+ neo test --maxWorkers=4
422
+ neo test --runInBand
421
423
  neo test --reporter=json --outputFile=.neo/test-results.json
422
424
  ```
423
425
 
424
- The JSON output file is written atomically. Human and JSON reports break timing
425
- into total, test-code, and framework-startup durations; human output also gives
426
- shared startup its own row instead of charging it to the first test.
426
+ Spec files run in parallel with up to four available CPU workers by default.
427
+ Set `test.maxWorkers` in `neo.json` or override it with `--maxWorkers <positive
428
+ integer>`. `--runInBand` overrides both and runs serially. A single selected
429
+ file runs in the main process. Hooks and tests within each file remain ordered;
430
+ fixtures and mocks stay isolated. Reports keep file and test registration order.
431
+
432
+ The JSON output file is written atomically. Reports distinguish elapsed startup
433
+ and execution from summed test work, which can exceed elapsed time in parallel
434
+ runs. JSON `timings.workerStartupMs` measures worker initialization and
435
+ `timings.executeMs` measures elapsed execution.
436
+
437
+ Tests compile and validate the local candidate once. They omit the extra source
438
+ serialization and re-lowering rehearsal used by `neo push --dry-run`; production
439
+ syntax, type checks, and whole-graph validation remain required. Spec compilation
440
+ shares one immutable compiler context and schema index across cache misses.
427
441
 
428
442
  Test compilation metadata and the experimental verified prepared-project cache
429
443
  live in `.neo/test-build/`; neither they nor test files are server records.
package/dist/neo.mjs CHANGED
@@ -38122,6 +38122,7 @@ var init_args = __esm({
38122
38122
  "generate-ids",
38123
38123
  "help",
38124
38124
  "passWithNoTests",
38125
+ "runInBand",
38125
38126
  "no-verify",
38126
38127
  "fix",
38127
38128
  "yes"
@@ -38173,9 +38174,11 @@ var init_args = __esm({
38173
38174
  "outputFile",
38174
38175
  "output-file",
38175
38176
  "testNamePattern",
38177
+ "maxWorkers",
38176
38178
  "test-timeout",
38177
38179
  "testTimeout",
38178
38180
  "passWithNoTests",
38181
+ "runInBand",
38179
38182
  "no-verify"
38180
38183
  ]);
38181
38184
  TEST_ONLY_FLAGS = /* @__PURE__ */ new Set([
@@ -38183,9 +38186,11 @@ var init_args = __esm({
38183
38186
  "outputFile",
38184
38187
  "output-file",
38185
38188
  "testNamePattern",
38189
+ "maxWorkers",
38186
38190
  "test-timeout",
38187
38191
  "testTimeout",
38188
- "passWithNoTests"
38192
+ "passWithNoTests",
38193
+ "runInBand"
38189
38194
  ]);
38190
38195
  }
38191
38196
  });
@@ -145290,6 +145295,113 @@ var init_project_integrity = __esm({
145290
145295
  }
145291
145296
  });
145292
145297
 
145298
+ // src/test-worker-pool.ts
145299
+ import { Worker as Worker5 } from "node:worker_threads";
145300
+ var TestWorkerPool;
145301
+ var init_test_worker_pool = __esm({
145302
+ "src/test-worker-pool.ts"() {
145303
+ "use strict";
145304
+ TestWorkerPool = class {
145305
+ constructor(workerUrl) {
145306
+ this.workerUrl = workerUrl;
145307
+ }
145308
+ workerUrl;
145309
+ interrupted = false;
145310
+ workers = [];
145311
+ failures = /* @__PURE__ */ new Map();
145312
+ pending = /* @__PURE__ */ new Map();
145313
+ async start(document, count) {
145314
+ await Promise.all(
145315
+ Array.from({ length: count }, async () => {
145316
+ const worker = new Worker5(this.workerUrl);
145317
+ this.workers.push(worker);
145318
+ worker.on(
145319
+ "error",
145320
+ (error) => this.fail(
145321
+ worker,
145322
+ error instanceof Error ? error : new Error(String(error))
145323
+ )
145324
+ );
145325
+ worker.on(
145326
+ "exit",
145327
+ (code) => this.fail(
145328
+ worker,
145329
+ new Error(
145330
+ `Test worker exited before completing its file (code ${code}).`
145331
+ )
145332
+ )
145333
+ );
145334
+ const response = await this.request(worker, { kind: "init", document });
145335
+ if (response.kind !== "ready")
145336
+ throw new Error("Test worker did not initialize its project.");
145337
+ })
145338
+ );
145339
+ }
145340
+ fail(worker, error) {
145341
+ this.failures.set(worker, error);
145342
+ this.pending.get(worker)?.(error);
145343
+ }
145344
+ request(worker, request) {
145345
+ const failure = this.failures.get(worker);
145346
+ if (failure !== void 0) return Promise.reject(failure);
145347
+ return new Promise((resolve5, reject) => {
145348
+ const cleanup = () => {
145349
+ worker.off("message", onMessage);
145350
+ this.pending.delete(worker);
145351
+ };
145352
+ const onMessage = (response) => {
145353
+ cleanup();
145354
+ if (response.kind === "error") reject(new Error(response.message));
145355
+ else resolve5(response);
145356
+ };
145357
+ const onError = (error) => {
145358
+ cleanup();
145359
+ reject(error);
145360
+ };
145361
+ this.pending.set(worker, onError);
145362
+ worker.once("message", onMessage);
145363
+ try {
145364
+ worker.postMessage(request);
145365
+ } catch (error) {
145366
+ onError(error instanceof Error ? error : new Error(String(error)));
145367
+ }
145368
+ });
145369
+ }
145370
+ async run(jobs, timeoutMs, onResult) {
145371
+ let next = 0;
145372
+ await Promise.all(
145373
+ this.workers.map(async (worker) => {
145374
+ while (!this.interrupted && next < jobs.length) {
145375
+ const job = jobs[next++];
145376
+ const response = await this.request(worker, {
145377
+ kind: "run",
145378
+ ...job,
145379
+ timeoutMs
145380
+ });
145381
+ if (response.kind !== "result")
145382
+ throw new Error("Test worker returned no file result.");
145383
+ onResult(job.spec, response.result);
145384
+ }
145385
+ })
145386
+ );
145387
+ }
145388
+ interrupt() {
145389
+ this.interrupted = true;
145390
+ for (const worker of this.workers) {
145391
+ worker.postMessage({ kind: "interrupt" });
145392
+ }
145393
+ }
145394
+ async stop() {
145395
+ for (const reject of this.pending.values())
145396
+ reject(new Error("Test worker stopped."));
145397
+ await Promise.all(this.workers.map((worker) => worker.terminate()));
145398
+ this.workers.length = 0;
145399
+ this.failures.clear();
145400
+ }
145401
+ };
145402
+ }
145403
+ });
145404
+
145293
145405
  // src/neoscript-language-adapter.ts
145294
145406
  function createCliNeoScriptContext(options) {
145295
145407
  const members = [...options.documents.members];
@@ -145299,12 +145411,7 @@ function createCliNeoScriptContext(options) {
145299
145411
  members,
145300
145412
  classes: [...options.documents.classes],
145301
145413
  enums: [...options.documents.enums],
145302
- interfaces: [...options.documents.interfaces],
145303
- databaseVM: {
145304
- memberById: (id2) => members.find(
145305
- (member) => member.id === id2
145306
- ) ?? null
145307
- }
145414
+ interfaces: [...options.documents.interfaces]
145308
145415
  };
145309
145416
  const compilerContext = createNeoScriptDocumentContext({
145310
145417
  vm,
@@ -145346,7 +145453,7 @@ async function validateCliNeoScript(uri, source, context) {
145346
145453
  service.closeDocument(uri);
145347
145454
  }
145348
145455
  }
145349
- function compileCliNeoScript(uri, source, context) {
145456
+ function compileCliNeoScript(uri, source, context, projectIndex) {
145350
145457
  const result = compileNeoScriptStrict(
145351
145458
  {
145352
145459
  uri,
@@ -145354,7 +145461,8 @@ function compileCliNeoScript(uri, source, context) {
145354
145461
  version: 1,
145355
145462
  text: source
145356
145463
  },
145357
- context
145464
+ context,
145465
+ { projectIndex }
145358
145466
  );
145359
145467
  const error = result.diagnostics.find(
145360
145468
  (diagnostic) => diagnostic.severity === "error"
@@ -147354,7 +147462,7 @@ var init_registry2 = __esm({
147354
147462
  "../packages/manifest/schema-contract/registry.mjs"() {
147355
147463
  "use strict";
147356
147464
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
147357
- cliVersion: "0.62.1",
147465
+ cliVersion: "0.62.2",
147358
147466
  projectFileUploadBatchSize: 32,
147359
147467
  documentRecords: {
147360
147468
  member: {
@@ -148773,8 +148881,10 @@ var init_registry2 = __esm({
148773
148881
  // src/commands/test.ts
148774
148882
  var test_exports = {};
148775
148883
  __export(test_exports, {
148884
+ executeTestFile: () => executeTestFile,
148776
148885
  inspectNeoTestCompilation: () => inspectNeoTestCompilation,
148777
148886
  maintainNeoTestBuildCache: () => maintainNeoTestBuildCache,
148887
+ prepareTestEvaluator: () => prepareTestEvaluator,
148778
148888
  runTest: () => runTest
148779
148889
  });
148780
148890
  import { createHash as createHash10, randomUUID as randomUUID6 } from "node:crypto";
@@ -148798,6 +148908,7 @@ import {
148798
148908
  resolve as resolve4,
148799
148909
  sep as sep8
148800
148910
  } from "node:path";
148911
+ import { availableParallelism } from "node:os";
148801
148912
  import { isDeepStrictEqual } from "node:util";
148802
148913
  function isRecord11(value) {
148803
148914
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -149119,8 +149230,28 @@ function cacheTestCandidate(workspace, inputFingerprint, candidate) {
149119
149230
  );
149120
149231
  return { ...candidate, documentSha256 };
149121
149232
  }
149122
- function compileSpec(workspace, document, absolutePath, projectCompilationHash2) {
149123
- const scriptDocument = readDocumentArrays(document);
149233
+ function createSpecCompiler(document) {
149234
+ const context = withNeoTestPrelude({
149235
+ ...createCliNeoScriptContext({
149236
+ documents: readDocumentArrays(document),
149237
+ kind: "function",
149238
+ thisClass: null
149239
+ }),
149240
+ kind: "function",
149241
+ projectSourceKind: "spec",
149242
+ returnType: { kind: "primitive", name: "void" }
149243
+ });
149244
+ const projectIndex = createProjectIndex(context.project);
149245
+ return (uri, source) => compileCliNeoScript(uri, source, context, projectIndex);
149246
+ }
149247
+ function lazySpecCompiler(document) {
149248
+ let compiler;
149249
+ return (uri, source) => {
149250
+ compiler ??= createSpecCompiler(document);
149251
+ return compiler(uri, source);
149252
+ };
149253
+ }
149254
+ function compileSpec(workspace, compile, absolutePath, projectCompilationHash2) {
149124
149255
  const path = relative8(workspace.root, absolutePath).split(sep8).join("/");
149125
149256
  const source = readFileSync19(absolutePath, "utf8");
149126
149257
  const sourceHash = createHash10("sha256").update(source).digest("hex");
@@ -149143,21 +149274,10 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
149143
149274
  }
149144
149275
  } catch {
149145
149276
  }
149146
- const baseContext2 = createCliNeoScriptContext({
149147
- documents: scriptDocument,
149148
- kind: "function",
149149
- thisClass: null
149150
- });
149151
- const context = withNeoTestPrelude({
149152
- ...baseContext2,
149153
- kind: "function",
149154
- projectSourceKind: "spec",
149155
- returnType: { kind: "primitive", name: "void" }
149156
- });
149157
149277
  const compiled = {
149158
149278
  path,
149159
149279
  source,
149160
- action: compileCliNeoScript(`file://${absolutePath}`, source, context)
149280
+ action: compile(`file://${absolutePath}`, source)
149161
149281
  };
149162
149282
  atomicWrite(
149163
149283
  artifactPath,
@@ -149837,6 +149957,18 @@ function failureFor(error, file, position, member = null) {
149837
149957
  function suiteHasSelectedTests(suite, selected) {
149838
149958
  return suite.tests.some((test) => selected.has(test.id)) || suite.suites.some((child) => suiteHasSelectedTests(child, selected));
149839
149959
  }
149960
+ function prepareTestEvaluator(document) {
149961
+ sharedEvaluatorBase(document);
149962
+ }
149963
+ async function executeTestFile(spec, document, selected, timeoutMs, interrupted = () => false) {
149964
+ return executeRegisteredSpec(
149965
+ registerSpec(spec, document),
149966
+ document,
149967
+ selected,
149968
+ timeoutMs,
149969
+ interrupted
149970
+ );
149971
+ }
149840
149972
  async function executeRegisteredSpec(registered, document, selected, timeoutMs, interrupted) {
149841
149973
  const results = [];
149842
149974
  const fileFailures = [];
@@ -150035,8 +150167,11 @@ async function runTest(workspace, options, dependencies = {}) {
150035
150167
  let testDurationMs = 0;
150036
150168
  let exitCode = 0;
150037
150169
  let interrupted = false;
150170
+ let workerPool;
150171
+ let parallelExecution = false;
150038
150172
  const handleSigint = () => {
150039
150173
  interrupted = true;
150174
+ workerPool?.interrupt();
150040
150175
  };
150041
150176
  const assertNotInterrupted = () => {
150042
150177
  if (interrupted) {
@@ -150051,6 +150186,7 @@ async function runTest(workspace, options, dependencies = {}) {
150051
150186
  select: 0,
150052
150187
  compile: 0,
150053
150188
  register: 0,
150189
+ workers: 0,
150054
150190
  execute: 0,
150055
150191
  finalize: 0
150056
150192
  };
@@ -150081,6 +150217,9 @@ async function runTest(workspace, options, dependencies = {}) {
150081
150217
  }
150082
150218
  if (candidate === null) {
150083
150219
  const prepared = dependencies.prepareLocalCandidate === void 0 ? await prepareLocalCandidateV4(workspace, {
150220
+ // Tests need the compiled, validated candidate. Serializing it
150221
+ // back to source and re-lowering is a push dry-run check.
150222
+ fullValidation: false,
150084
150223
  onPhase: (label) => {
150085
150224
  dependencies.progress?.update(label);
150086
150225
  }
@@ -150132,8 +150271,9 @@ async function runTest(workspace, options, dependencies = {}) {
150132
150271
  );
150133
150272
  }
150134
150273
  enterPhase("compile", "Compiling test files\u2026");
150274
+ const compile = lazySpecCompiler(rawDocument);
150135
150275
  specs = selectedPaths.map(
150136
- (path) => compileSpec(workspace, rawDocument, path, compilationHash)
150276
+ (path) => compileSpec(workspace, compile, path, compilationHash)
150137
150277
  );
150138
150278
  let pattern = null;
150139
150279
  if (options.testNamePattern !== null) {
@@ -150162,21 +150302,54 @@ async function runTest(workspace, options, dependencies = {}) {
150162
150302
  );
150163
150303
  }
150164
150304
  const selectedIds = new Set(selectedTests.map((test) => test.id));
150165
- enterPhase("execute", "Running tests\u2026");
150166
150305
  const timeoutMs = options.timeoutMs ?? workspace.config.test?.timeoutMs ?? 5e3;
150167
- for (const entry of registered) {
150168
- const executed = await executeRegisteredSpec(
150169
- entry,
150170
- rawDocument,
150171
- selectedIds,
150172
- timeoutMs,
150173
- () => interrupted
150174
- );
150306
+ const maxWorkers = options.maxWorkers ?? workspace.config.test?.maxWorkers ?? Math.min(4, availableParallelism());
150307
+ if (!Number.isSafeInteger(maxWorkers) || maxWorkers <= 0) {
150308
+ throw new NeoTestUsageError("--maxWorkers must be a positive integer.");
150309
+ }
150310
+ const jobs = registered.filter(
150311
+ (entry) => (selectedTestCountByFile.get(entry.spec.path) ?? 0) > 0
150312
+ );
150313
+ const workerCount = options.runInBand === true ? 1 : Math.min(maxWorkers, jobs.length);
150314
+ const recordResult = (spec, executed) => {
150175
150315
  results.push(...executed.tests);
150176
150316
  testDurationMs += executed.testDurationMs;
150177
- fileFailuresByPath.set(entry.spec.path, executed.failures);
150317
+ fileFailuresByPath.set(spec.path, executed.failures);
150318
+ };
150319
+ if (workerCount > 1) {
150320
+ enterPhase("workers", "Starting test workers\u2026");
150321
+ workerPool = new TestWorkerPool(
150322
+ new URL("./test-worker.mjs", import.meta.url)
150323
+ );
150324
+ await workerPool.start(rawDocument, workerCount);
150178
150325
  assertNotInterrupted();
150326
+ parallelExecution = true;
150327
+ enterPhase("execute", "Running tests\u2026");
150328
+ await workerPool.run(
150329
+ jobs.map((entry) => ({
150330
+ spec: entry.spec,
150331
+ selectedIds: entry.tests.filter((test) => selectedIds.has(test.id)).map((test) => test.id)
150332
+ })),
150333
+ timeoutMs,
150334
+ recordResult
150335
+ );
150336
+ } else {
150337
+ enterPhase("execute", "Running tests\u2026");
150338
+ for (const entry of jobs) {
150339
+ recordResult(
150340
+ entry.spec,
150341
+ await executeRegisteredSpec(
150342
+ entry,
150343
+ rawDocument,
150344
+ selectedIds,
150345
+ timeoutMs,
150346
+ () => interrupted
150347
+ )
150348
+ );
150349
+ assertNotInterrupted();
150350
+ }
150179
150351
  }
150352
+ assertNotInterrupted();
150180
150353
  if (results.some((result) => result.status === "failed") || [...fileFailuresByPath.values()].some((failures) => failures.length > 0)) {
150181
150354
  exitCode = 1;
150182
150355
  }
@@ -150184,7 +150357,7 @@ async function runTest(workspace, options, dependencies = {}) {
150184
150357
  const buildManifest = {
150185
150358
  version: 1,
150186
150359
  cliVersion: PROJECT_SCHEMA_CONTRACT.cliVersion,
150187
- runnerRevision: 3,
150360
+ runnerRevision: 4,
150188
150361
  compilerRevision: NEOSCRIPT_COMPILER_REVISION,
150189
150362
  evaluatorRevision: 4,
150190
150363
  projectFingerprint,
@@ -150204,12 +150377,12 @@ async function runTest(workspace, options, dependencies = {}) {
150204
150377
  `
150205
150378
  );
150206
150379
  } catch (error) {
150207
- const diagnosticCode = error instanceof NeoTestInterruptedError ? "NEO_TEST_INTERRUPTED" : error instanceof NeoTestPreparedCandidateError ? "NEO_TEST_PREPARED_CANDIDATE" : error instanceof NeoTestUsageError ? "NEO_TEST_USAGE" : error instanceof NeoTestNoTestsError ? "NEO_TEST_NO_TESTS" : error instanceof NeoTestRegistrationError ? "NEO_TEST_REGISTRATION" : phase === "prepare" ? "NEO_TEST_PREPARATION" : phase === "compile" ? "NEO_TEST_COMPILE" : "NEO_TEST_RUNNER";
150208
- exitCode = error instanceof NeoTestInterruptedError ? 130 : error instanceof NeoTestUsageError || error instanceof NeoTestReportWriteError || error instanceof NeoTestPreparedCandidateError || diagnosticCode === "NEO_TEST_RUNNER" ? 2 : 1;
150380
+ const diagnosticCode = interrupted || error instanceof NeoTestInterruptedError ? "NEO_TEST_INTERRUPTED" : error instanceof NeoTestPreparedCandidateError ? "NEO_TEST_PREPARED_CANDIDATE" : error instanceof NeoTestUsageError ? "NEO_TEST_USAGE" : error instanceof NeoTestNoTestsError ? "NEO_TEST_NO_TESTS" : error instanceof NeoTestRegistrationError ? "NEO_TEST_REGISTRATION" : phase === "prepare" ? "NEO_TEST_PREPARATION" : phase === "compile" ? "NEO_TEST_COMPILE" : "NEO_TEST_RUNNER";
150381
+ exitCode = interrupted || error instanceof NeoTestInterruptedError ? 130 : error instanceof NeoTestUsageError || error instanceof NeoTestReportWriteError || error instanceof NeoTestPreparedCandidateError || diagnosticCode === "NEO_TEST_RUNNER" ? 2 : 1;
150209
150382
  diagnostics.push({
150210
150383
  severity: "error",
150211
150384
  code: diagnosticCode,
150212
- message: errorMessage2(error),
150385
+ message: interrupted ? "Neo test was interrupted by SIGINT; partial results were written." : errorMessage2(error),
150213
150386
  file: null,
150214
150387
  line: null,
150215
150388
  column: null,
@@ -150217,6 +150390,7 @@ async function runTest(workspace, options, dependencies = {}) {
150217
150390
  });
150218
150391
  }
150219
150392
  finishPhase();
150393
+ await workerPool?.stop();
150220
150394
  dependencies.progress?.stop();
150221
150395
  process.off("SIGINT", handleSigint);
150222
150396
  const failed = results.filter((result) => result.status === "failed").length;
@@ -150226,21 +150400,33 @@ async function runTest(workspace, options, dependencies = {}) {
150226
150400
  );
150227
150401
  const skipped = Math.max(0, selectedTestTotal - results.length);
150228
150402
  const durationMs = performance.now() - performanceStarted;
150229
- const startupDurationMs = Math.max(0, durationMs - testDurationMs);
150403
+ const startupDurationMs = parallelExecution ? Math.max(0, durationMs - phaseDurations.execute) : Math.max(0, durationMs - testDurationMs);
150230
150404
  const perTestStartupDurationMs = results.reduce(
150231
150405
  (sum, result) => sum + result.startupDurationMs,
150232
150406
  0
150233
150407
  );
150234
150408
  const sharedStartupDurationMs = Math.max(
150235
150409
  0,
150236
- startupDurationMs - perTestStartupDurationMs
150410
+ startupDurationMs - (parallelExecution ? 0 : perTestStartupDurationMs)
150237
150411
  );
150412
+ const testsByFile = /* @__PURE__ */ new Map();
150413
+ for (const result of results) {
150414
+ const tests = testsByFile.get(result.file) ?? [];
150415
+ tests.push(result);
150416
+ testsByFile.set(result.file, tests);
150417
+ }
150238
150418
  const testFiles = specs.map((spec) => {
150239
- const tests = results.filter((result) => result.file === spec.path);
150419
+ const tests = testsByFile.get(spec.path) ?? [];
150240
150420
  const failures = fileFailuresByPath.get(spec.path) ?? [];
150421
+ let status = "passed";
150422
+ if (tests.some((test) => test.status === "failed") || failures.length > 0) {
150423
+ status = "failed";
150424
+ } else if (tests.length < (selectedTestCountByFile.get(spec.path) ?? 0) && exitCode !== 0) {
150425
+ status = interrupted ? "interrupted" : "failed";
150426
+ }
150241
150427
  return {
150242
150428
  path: spec.path,
150243
- status: tests.some((test) => test.status === "failed") || failures.length > 0 ? "failed" : interrupted && tests.length < (selectedTestCountByFile.get(spec.path) ?? 0) ? "interrupted" : "passed",
150429
+ status,
150244
150430
  tests,
150245
150431
  failures
150246
150432
  };
@@ -150276,6 +150462,7 @@ async function runTest(workspace, options, dependencies = {}) {
150276
150462
  selectMs: phaseDurations.select,
150277
150463
  compileMs: phaseDurations.compile,
150278
150464
  registerMs: phaseDurations.register,
150465
+ workerStartupMs: phaseDurations.workers,
150279
150466
  executeMs: phaseDurations.execute,
150280
150467
  finalizeMs: phaseDurations.finalize
150281
150468
  },
@@ -150357,8 +150544,9 @@ async function runTest(workspace, options, dependencies = {}) {
150357
150544
  console.log(
150358
150545
  ` ${color.bold(pad("Tests", 11))} ${formatStatusCounts(report.summary.passed, report.summary.failed, report.summary.tests, report.summary.skipped)}`
150359
150546
  );
150547
+ const timingDetail = parallelExecution ? `test work ${formatTestDuration(report.summary.testDurationMs)}, execution ${formatTestDuration(report.timings.executeMs)}, startup ${formatTestDuration(report.summary.startupDurationMs)}` : `test ${formatTestDuration(report.summary.testDurationMs)}, startup ${formatTestDuration(report.summary.startupDurationMs)}`;
150360
150548
  console.log(
150361
- ` ${color.bold(pad("Duration", 11))} ${color.bold(`total ${formatTestDuration(report.summary.durationMs)}`)} ${color.dim(`(test ${formatTestDuration(report.summary.testDurationMs)}, startup ${formatTestDuration(report.summary.startupDurationMs)})`)}`
150549
+ ` ${color.bold(pad("Duration", 11))} ${color.bold(`total ${formatTestDuration(report.summary.durationMs)}`)} ${color.dim(`(${timingDetail})`)}`
150362
150550
  );
150363
150551
  }
150364
150552
  if (!report.success) process.exitCode = exitCode;
@@ -150388,9 +150576,10 @@ async function inspectNeoTestCompilation(workspace, dependencies = {}) {
150388
150576
  document
150389
150577
  );
150390
150578
  const errors = [];
150579
+ const compile = lazySpecCompiler(document);
150391
150580
  for (const path of selectedPaths) {
150392
150581
  try {
150393
- compileSpec(workspace, document, path, compilationHash);
150582
+ compileSpec(workspace, compile, path, compilationHash);
150394
150583
  } catch (error) {
150395
150584
  errors.push({
150396
150585
  file: relative8(workspace.root, path).split(sep8).join("/"),
@@ -150409,6 +150598,7 @@ var TEST_BUILD_CACHE_LIMIT_BYTES, ABANDONED_TEMP_MAX_AGE_MS, TEST_CANDIDATE_CACH
150409
150598
  var init_test = __esm({
150410
150599
  "src/commands/test.ts"() {
150411
150600
  "use strict";
150601
+ init_test_worker_pool();
150412
150602
  init_src();
150413
150603
  init_members();
150414
150604
  init_neoscript2();
@@ -155947,6 +156137,11 @@ ${h("Usage")}
155947
156137
  neo test ${d("[file-or-directory ...]")}
155948
156138
  neo test -t ${d('"name pattern"')}
155949
156139
  neo test ${d("--reporter=json --outputFile=output.json")}
156140
+ neo test ${d("--maxWorkers=4")}
156141
+ neo test ${d("--runInBand")}
156142
+
156143
+ Files run in parallel, using up to four available CPU workers by default.
156144
+ --maxWorkers overrides neo.json test.maxWorkers; --runInBand runs serially.
155950
156145
 
155951
156146
  Tests execute against the locally compiled candidate graph, including changes
155952
156147
  that have not been pushed. Test files are never included in a push.
@@ -156034,7 +156229,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
156034
156229
  async function main() {
156035
156230
  const args = parseArgs(process.argv.slice(2));
156036
156231
  if (args.command === "--version") {
156037
- console.log("0.62.1");
156232
+ console.log("0.62.2");
156038
156233
  return;
156039
156234
  }
156040
156235
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
@@ -156360,6 +156555,11 @@ async function main() {
156360
156555
  "--testTimeout must be a positive number of milliseconds."
156361
156556
  );
156362
156557
  }
156558
+ const maxWorkersValue = stringFlag(args, "maxWorkers");
156559
+ const maxWorkers = maxWorkersValue === null ? void 0 : Number(maxWorkersValue);
156560
+ if (maxWorkers !== void 0 && (!Number.isSafeInteger(maxWorkers) || maxWorkers <= 0)) {
156561
+ throw new NeoCliUsageError("--maxWorkers must be a positive integer.");
156562
+ }
156363
156563
  const progress = reporterValue === "default" && process.stdout.isTTY === true ? spinner("Reading local workspace\u2026") : null;
156364
156564
  try {
156365
156565
  const workspace = loadWorkspaceForCommand(args);
@@ -156372,6 +156572,8 @@ async function main() {
156372
156572
  reporter: reporterValue,
156373
156573
  outputFile: stringFlag(args, "outputFile") ?? stringFlag(args, "output-file"),
156374
156574
  passWithNoTests: boolFlag(args, "passWithNoTests"),
156575
+ runInBand: boolFlag(args, "runInBand"),
156576
+ ...maxWorkers === void 0 ? {} : { maxWorkers },
156375
156577
  ...timeoutMs === void 0 ? {} : { timeoutMs }
156376
156578
  },
156377
156579
  progress === null ? {} : { progress }