openvibers/openvibe.shared Commits

Add parallel 'auto' to the test-runner and release 2.8.0 (#16)

OP OpenVibers committed 5 Oct 2026 · ae68e7a4f5 · 7 files, +46 −10

Asked: Part of a cross-repository initiative (init-every-service-repository-s-f-1005T0018): Every service repository's full verify (test and test-pg) finishes in 10 minutes or less on this machine, so pipelines are never held by a 30-minute check. How it works when done: openvibe-shared's test-runner pick…
Harness job init-every-service-re-0018-s1-002144.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f6a5752..5d77042 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
++## 2.8.0 — 2026-10-04
++
++- `openvibe-shared/test-runner`: `parallel: 'auto'` resolves to `max(1, min(8, cpus - 1))` (before, any
++ non-number made the runner start nothing); a numeric pin works as before and no option keeps the
++ `min(4, cpus - 1)` default. `run()` also returns the resolved `parallel`, and `resolveParallel()` is
++ exported. Each test file runs in its own process and owns its database (`createTestDb`: its own PGlite,
++ or under `test:pg` its own PostgreSQL schema, roles and port), so files parallelize safely and a repo
++ no longer needs `parallel: 1`.
++
## 2.7.0 — 2026-10-04
- `openvibe-shared/notifications`: a new `CONFIRMATION_REQUESTED` type (category `system`, priority
ddiff --git a/README.md b/README.md
iindex 9f54933..730f4cb 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v2.7.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v2.8.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex fd09c3f..166ea01 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "2.7.0",
++ "version": "2.8.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/package-lock.json b/package-lock.json
iindex e1af400..cb18563 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "2.7.0",
++ "version": "2.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "2.7.0",
++ "version": "2.8.0",
"license": "MIT",
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
ddiff --git a/package.json b/package.json
iindex df09d6b..d3538ca 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "2.7.0",
++ "version": "2.8.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test-runner.js b/test-runner.js
iindex 5af62c3..d6920a0 100644
---- a/test-runner.js
++++ b/test-runner.js
@@@ -11,6 +11,11 @@
// npm test -- quota cache # only files whose name contains one of the words
// npm test -- --strict # a skip fails the run too (or OV_TEST_STRICT=1)
//
++// Each test file runs in its own process and owns its database: openvibe-sdk/testing `createTestDb` gives it its
++// own PGlite, or under `test:pg` its own PostgreSQL schema, roles and port. Files therefore parallelize safely and
++// a repo needs no `parallel: 1` pin. `parallel` is a number (a pin), `'auto'` (min(8, cpus-1), at least 1) or unset
++// (min(4, cpus-1), at least 1).
++//
// A test that cannot run something (no ffmpeg, no Chrome, no git history) prints one line per thing it
// skipped, `<label>: skipped (<why>)`, and still exits 0. Such a file is listed with ○ and its reasons,
// and is NOT counted as passed: the summary reads `148/150 test files passed, 2 skipped (…)`. Only a run
@@@ -35,6 +40,15 @@ function skipsIn(output) {
return out;
}
++/** How many files run at once: a numeric pin as given (at least 1), 'auto' by CPUs (1..8), else the default (1..4). */
++function resolveParallel(parallel) {
++ const cpus = os.cpus().length;
++ if (parallel === 'auto') return Math.max(1, Math.min(8, cpus - 1));
++ const n = Math.floor(Number(parallel));
++ if (Number.isFinite(n) && n >= 1) return n;
++ return Math.max(1, Math.min(4, cpus - 1));
++}
++
function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
return new Promise((resolve) => {
const started = Date.now();
@@@ -55,7 +69,7 @@ function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
/**
* Run the tests; resolves { results, passed, skipped, failed, strict, exitCode, summary } and prints as it goes.
* opts: dir (required: the test directory), argv (default process.argv.slice(2)), cwd (default dir/..),
-- * timeoutMs (60000), parallel (min(4, cpus-1)), env (added to process.env; NODE_ENV=test by default),
++ * timeoutMs (60000), parallel (a number, 'auto' = min(8, cpus-1), default min(4, cpus-1); resolved value returned), env (added to process.env; NODE_ENV=test by default),
* hide (a RegExp: output lines of a failed file left out, e.g. /^\[DB\] /), pad (file-name column, 44),
* match (which files, default /\.test\.js$/), serial (files to run alone after parallel files),
* nodeArgs ([]), log (console.log).
@@@ -78,7 +92,7 @@ async function run(opts = {}) {
dir, cwd: opts.cwd || path.join(dir, '..'), timeoutMs: opts.timeoutMs || 60000, nodeArgs: opts.nodeArgs || [],
env: { ...process.env, NODE_ENV: 'test', ...(opts.env || {}) },
};
-- const parallel = Math.max(1, opts.parallel || Math.min(4, os.cpus().length - 1));
++ const parallel = resolveParallel(opts.parallel);
const pad = opts.pad || 44;
const queue = [], serial = [];
for (const file of files) (opts.serial && opts.serial.test(file) ? serial : queue).push(file);
@@@ -110,7 +124,7 @@ async function run(opts = {}) {
: `${passed}/${results.length} test files passed`;
log(`\n${summary}`);
const exitCode = failed.length || (strict && skipped.length) ? 1 : 0;
-- return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary };
++ return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary, parallel };
}
/** run(), then exit with its status: what a test/run.js calls. */
@@@ -118,4 +132,4 @@ function main(opts) {
run(opts).then((r) => process.exit(r.exitCode), (err) => { console.error(err); process.exit(1); });
}
--module.exports = { run, main, skipsIn, SKIP_RE };
++module.exports = { run, main, skipsIn, resolveParallel, SKIP_RE };
ddiff --git a/test/test-runner.test.js b/test/test-runner.test.js
iindex 7e1991d..afba2be 100644
---- a/test/test-runner.test.js
++++ b/test/test-runner.test.js
@@@ -6,7 +6,7 @@ const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
--const { run, skipsIn } = require('../test-runner');
++const { run, skipsIn, resolveParallel } = require('../test-runner');
(async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-test-runner-'));
@@@ -43,6 +43,19 @@ const { run, skipsIn } = require('../test-runner');
assert.strictEqual(r.exitCode, 1);
assert.match(r.summary, /strict: skips fail the run$/);
++ // parallel: 'auto' resolves by CPU to a number >= 1 and still runs every matched file; pins and the default hold.
++ const cpus = os.cpus().length;
++ assert.strictEqual(resolveParallel('auto'), Math.max(1, Math.min(8, cpus - 1)));
++ assert.strictEqual(resolveParallel(undefined), Math.max(1, Math.min(4, cpus - 1)));
++ assert.strictEqual(resolveParallel(1), 1);
++ assert.strictEqual(resolveParallel(3), 3);
++ assert.strictEqual(resolveParallel('nonsense'), Math.max(1, Math.min(4, cpus - 1)));
++ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 'auto' });
++ assert.ok(Number.isInteger(r.parallel) && r.parallel >= 1);
++ assert.deepStrictEqual([r.results.length, r.summary, r.exitCode], [2, '2/2 test files passed', 0]);
++ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 1 });
++ assert.deepStrictEqual([r.parallel, r.results.length], [1, 2]);
++
r = await run({ dir, argv: ['nothing-matches'], log });
assert.strictEqual(r.exitCode, 1);
assert.deepStrictEqual(skipsIn('x\n ACK test: skipped (ffmpeg not installed) \nrollback with newer writes: skipped (no git history)\nOK B: skipped — old style'),