openvibers/openvibe.ai Commits
npm test no longer counts a skipped test as passed (roadmap WS-Q task 7; openvibe-shared/test-runner, Shared v1.25.0). test/run.js counted every file that exited 0 as passed, so media-analysis without ffmpeg, which runs only its parser part, read as green. It is now the shared runner (same files, one at a time as before, 60 s timeout per file): a test prints "<label>: skipped (<why>)" for what it could not run, and that file is listed with ○ and its reasons and not counted as passed; --strict makes a skip fail the run. media-analysis's four ffmpeg parts printed "(skipped: no ffmpeg/ffprobe)" and now print "media-analysis whisper language / real run / local model down / free disk check: skipped (no ffmpeg/ffprobe)"; workflows.test.js left media.analyze out of its end-to-end run without a word when ffprobe is missing and now prints "workflow media.analyze end to end: skipped (no ffprobe)". Without ffmpeg the run reads 22/24 test files passed, 2 skipped. openvibe-shared is pinned at v1.25.0 (from v1.22.0; 1.23 and 1.24 add the web runtime, release-watch fixes and the readiness skip, and /api/ready answers as before because its db check really runs; the README follows). npm test (Node 22, ffmpeg installed) 24/24.
Changes
@@@ -42,7 +42,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.- provider APIs configured by the operator (OpenAI-compatible, Anthropic, the HTTP seam), whisper.cppand the local model server `openvibe-llm.service` on this host- `openvibe-contracts` v0.75.0, `openvibe-sdk` v0.11.0 (service tokens, events outbox),-- `openvibe-shared` v1.22.0, pinned by release tarball++ `openvibe-shared` v1.25.0, pinned by release tarball## Capabilities@@@ -15,7 +15,7 @@"express": "^4.21.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0"},"engines": {"node": ">=20.0.0"@@@ -1420,9 +1420,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.22.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",++ "version": "1.25.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",++ "integrity": "sha512-oqZ+PvljrWi8IEbtg6bDYai17ZV2sALFx/ezMptniELSr10TgFUiGUQt+b1IWwiHv0T2Y1U2iSrkirJxMNgjSw==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -21,7 +21,7 @@"express": "^4.21.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0"},"optionalDependencies": {"sharp": "^0.33.5"@@@ -71,7 +71,7 @@ t.test('the parser reads every filter\'s lines; loud means well above the typica});t.test('whisper reports the language it detected, not "auto"', async () => {-- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');++ if (!HAS_FFMPEG) return console.log('media-analysis whisper language: skipped (no ffmpeg/ffprobe)');const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ov-ai-whisper-'));try {const bin = path.join(dir, 'whisper-cli');@@@ -90,7 +90,7 @@ t.test('whisper reports the language it detected, not "auto"', async () => {});t.test('a real run: signals, scenes, highlights with evidence, and the local model\'s overview', async () => {-- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');++ if (!HAS_FFMPEG) return console.log('media-analysis real run: skipped (no ffmpeg/ffprobe)');media = nodeHttp.createServer((req, res) => { res.writeHead(req.url === '/fixture.mp4' ? 200 : 404, { 'Content-Type': 'video/mp4' }); res.end(req.url === '/fixture.mp4' ? FIXTURE : ''); });await new Promise((r) => media.listen(0, '127.0.0.1', r));mediaBase = `http://127.0.0.1:${media.address().port}`;@@@ -127,7 +127,7 @@ t.test('a real run: signals, scenes, highlights with evidence, and the local mod});t.test('local model down: extractive, and it says so; paid only with allow_paid and a budget, never past it', async () => {-- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');++ if (!HAS_FFMPEG) return console.log('media-analysis local model down: skipped (no ffmpeg/ffprobe)');const local = await seamServer(() => ({ status: 500, body: { error: 'model not loaded' } }));const paid = await seamServer(() => completion('Paid overview of the recording.'));const h = await boot({ env: {@@@ -174,7 +174,7 @@ t.test('local model down: extractive, and it says so; paid only with allow_paid});t.test('too little free disk refuses before downloading', async () => {-- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');++ if (!HAS_FFMPEG) return console.log('media-analysis free disk check: skipped (no ffmpeg/ffprobe)');let fetched = 0;const counting = nodeHttp.createServer((req, res) => { fetched++; res.writeHead(200, { 'Content-Type': 'video/mp4' }); res.end(FIXTURE); });await new Promise((r) => counting.listen(0, '127.0.0.1', r));@@@ -7,51 +7,10 @@** npm test # everything* npm test -- quota cache # only files whose name contains one of the words++ * npm test -- --strict # a skipped test fails the run too++ *++ * A test that cannot run something here prints `<label>: skipped (<why>)`: that file is listed with++ * ○ and not counted as passed (openvibe-shared/test-runner).*/'use strict';--const { spawn } = require('child_process');--const fs = require('fs');--const path = require('path');----const filters = process.argv.slice(2).filter((a) => !a.startsWith('-'));--const files = fs.readdirSync(__dirname)-- .filter((f) => f.endsWith('.test.js'))-- .filter((f) => !filters.length || filters.some((w) => f.includes(w)))-- .sort();--const TIMEOUT_MS = 60000;----function runOne(file) {-- return new Promise((resolve) => {-- const started = Date.now();-- const child = spawn(process.execPath, [path.join(__dirname, file)], {-- cwd: path.join(__dirname, '..'),-- env: { ...process.env, NODE_ENV: 'test' },-- stdio: ['ignore', 'pipe', 'pipe'],-- });-- let output = '';-- child.stdout.on('data', (c) => { output += c; });-- child.stderr.on('data', (c) => { output += c; });-- const timer = setTimeout(() => { output += `\n[run] timed out after ${TIMEOUT_MS}ms`; child.kill('SIGKILL'); }, TIMEOUT_MS);-- child.on('close', (code, signal) => {-- clearTimeout(timer);-- resolve({ file, ok: code === 0, code: code ?? signal, ms: Date.now() - started, output });-- });-- });--}----(async () => {-- if (!files.length) { console.error('no test files matched'); process.exit(1); }-- const results = [];-- for (const f of files) {-- const r = await runOne(f);-- results.push(r);-- console.log(`${r.ok ? '✓' : '✗'} ${r.file.padEnd(32)} ${String(r.ms).padStart(6)}ms`);-- }-- const failed = results.filter((r) => !r.ok);-- for (const r of failed) {-- console.log(`\n── ${r.file} (exit ${r.code}) ──`);-- console.log(r.output.split('\n').slice(-60).join('\n'));-- }-- console.log(`\n${results.length - failed.length}/${results.length} test files passed`);-- process.exit(failed.length ? 1 : 0);--})();++require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 60000, pad: 32, parallel: 1 });@@@ -106,7 +106,7 @@ t.test('every workflow has compiling, versioned input and output schemas and ant.test('every workflow runs end to end on the stub and returns schema-valid output', async () => {for (const w of h.registry.listWorkflows()) {-- if (w.key === 'media.analyze' && !HAS_FFMPEG) continue; // test/media-analysis.test.js says so++ if (w.key === 'media.analyze' && !HAS_FFMPEG) { console.log('workflow media.analyze end to end: skipped (no ffprobe)'); continue; }const r = await request(h.base, 'POST', '/api/v1/runs?wait=10000', { tok, body: { workflow: w.key, input: EXAMPLES[w.key] } });assert.strictEqual(r.status, 201, `${w.key}: ${r.status} ${r.text.slice(0, 300)}`);const run = r.body.run;
@@ -42,7 +42,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
@@ -42,7 +42,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
- provider APIs configured by the operator (OpenAI-compatible, Anthropic, the HTTP seam), whisper.cpp
- provider APIs configured by the operator (OpenAI-compatible, Anthropic, the HTTP seam), whisper.cpp
and the local model server `openvibe-llm.service` on this host
and the local model server `openvibe-llm.service` on this host
- `openvibe-contracts` v0.75.0, `openvibe-sdk` v0.11.0 (service tokens, events outbox),
- `openvibe-contracts` v0.75.0, `openvibe-sdk` v0.11.0 (service tokens, events outbox),
- `openvibe-shared` v1.22.0, pinned by release tarball
+ `openvibe-shared` v1.25.0, pinned by release tarball
## Capabilities
## Capabilities
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0"
},
},
"engines": {
"engines": {
"node": ">=20.0.0"
"node": ">=20.0.0"
@@ -1420,9 +1420,9 @@
@@ -1420,9 +1420,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.22.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
+ "version": "1.25.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0",
+ "integrity": "sha512-oqZ+PvljrWi8IEbtg6bDYai17ZV2sALFx/ezMptniELSr10TgFUiGUQt+b1IWwiHv0T2Y1U2iSrkirJxMNgjSw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.75.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.25.0"
},
},
"optionalDependencies": {
"optionalDependencies": {
"sharp": "^0.33.5"
"sharp": "^0.33.5"
@@ -71,7 +71,7 @@ t.test('the parser reads every filter\'s lines; loud means well above the typica
@@ -71,7 +71,7 @@ t.test('the parser reads every filter\'s lines; loud means well above the typica
});
});
t.test('whisper reports the language it detected, not "auto"', async () => {
t.test('whisper reports the language it detected, not "auto"', async () => {
- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');
+ if (!HAS_FFMPEG) return console.log('media-analysis whisper language: skipped (no ffmpeg/ffprobe)');
const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ov-ai-whisper-'));
const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ov-ai-whisper-'));
try {
try {
const bin = path.join(dir, 'whisper-cli');
const bin = path.join(dir, 'whisper-cli');
@@ -90,7 +90,7 @@ t.test('whisper reports the language it detected, not "auto"', async () => {
@@ -90,7 +90,7 @@ t.test('whisper reports the language it detected, not "auto"', async () => {
});
});
t.test('a real run: signals, scenes, highlights with evidence, and the local model\'s overview', async () => {
t.test('a real run: signals, scenes, highlights with evidence, and the local model\'s overview', async () => {
- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');
+ if (!HAS_FFMPEG) return console.log('media-analysis real run: skipped (no ffmpeg/ffprobe)');
media = nodeHttp.createServer((req, res) => { res.writeHead(req.url === '/fixture.mp4' ? 200 : 404, { 'Content-Type': 'video/mp4' }); res.end(req.url === '/fixture.mp4' ? FIXTURE : ''); });
media = nodeHttp.createServer((req, res) => { res.writeHead(req.url === '/fixture.mp4' ? 200 : 404, { 'Content-Type': 'video/mp4' }); res.end(req.url === '/fixture.mp4' ? FIXTURE : ''); });
await new Promise((r) => media.listen(0, '127.0.0.1', r));
await new Promise((r) => media.listen(0, '127.0.0.1', r));
mediaBase = `http://127.0.0.1:${media.address().port}`;
mediaBase = `http://127.0.0.1:${media.address().port}`;
@@ -127,7 +127,7 @@ t.test('a real run: signals, scenes, highlights with evidence, and the local mod
@@ -127,7 +127,7 @@ t.test('a real run: signals, scenes, highlights with evidence, and the local mod
});
});
t.test('local model down: extractive, and it says so; paid only with allow_paid and a budget, never past it', async () => {
t.test('local model down: extractive, and it says so; paid only with allow_paid and a budget, never past it', async () => {
- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');
+ if (!HAS_FFMPEG) return console.log('media-analysis local model down: skipped (no ffmpeg/ffprobe)');
const local = await seamServer(() => ({ status: 500, body: { error: 'model not loaded' } }));
const local = await seamServer(() => ({ status: 500, body: { error: 'model not loaded' } }));
const paid = await seamServer(() => completion('Paid overview of the recording.'));
const paid = await seamServer(() => completion('Paid overview of the recording.'));
const h = await boot({ env: {
const h = await boot({ env: {
@@ -174,7 +174,7 @@ t.test('local model down: extractive, and it says so; paid only with allow_paid
@@ -174,7 +174,7 @@ t.test('local model down: extractive, and it says so; paid only with allow_paid
});
});
t.test('too little free disk refuses before downloading', async () => {
t.test('too little free disk refuses before downloading', async () => {
- if (!HAS_FFMPEG) return console.log(' (skipped: no ffmpeg/ffprobe)');
+ if (!HAS_FFMPEG) return console.log('media-analysis free disk check: skipped (no ffmpeg/ffprobe)');
let fetched = 0;
let fetched = 0;
const counting = nodeHttp.createServer((req, res) => { fetched++; res.writeHead(200, { 'Content-Type': 'video/mp4' }); res.end(FIXTURE); });
const counting = nodeHttp.createServer((req, res) => { fetched++; res.writeHead(200, { 'Content-Type': 'video/mp4' }); res.end(FIXTURE); });
await new Promise((r) => counting.listen(0, '127.0.0.1', r));
await new Promise((r) => counting.listen(0, '127.0.0.1', r));
@@ -7,51 +7,10 @@
@@ -7,51 +7,10 @@
*
*
* npm test # everything
* npm test # everything
* npm test -- quota cache # only files whose name contains one of the words
* npm test -- quota cache # only files whose name contains one of the words
+ * npm test -- --strict # a skipped test fails the run too
+ *
+ * A test that cannot run something here prints `<label>: skipped (<why>)`: that file is listed with
+ * ○ and not counted as passed (openvibe-shared/test-runner).
*/
*/
'use strict';
'use strict';
-const { spawn } = require('child_process');
-const fs = require('fs');
-const path = require('path');
-
-const filters = process.argv.slice(2).filter((a) => !a.startsWith('-'));
-const files = fs.readdirSync(__dirname)
- .filter((f) => f.endsWith('.test.js'))
- .filter((f) => !filters.length || filters.some((w) => f.includes(w)))
- .sort();
-const TIMEOUT_MS = 60000;
-
-function runOne(file) {
- return new Promise((resolve) => {
- const started = Date.now();
- const child = spawn(process.execPath, [path.join(__dirname, file)], {
- cwd: path.join(__dirname, '..'),
- env: { ...process.env, NODE_ENV: 'test' },
- stdio: ['ignore', 'pipe', 'pipe'],
- });
- let output = '';
- child.stdout.on('data', (c) => { output += c; });
- child.stderr.on('data', (c) => { output += c; });
- const timer = setTimeout(() => { output += `\n[run] timed out after ${TIMEOUT_MS}ms`; child.kill('SIGKILL'); }, TIMEOUT_MS);
- child.on('close', (code, signal) => {
- clearTimeout(timer);
- resolve({ file, ok: code === 0, code: code ?? signal, ms: Date.now() - started, output });
- });
- });
-}
-
-(async () => {
- if (!files.length) { console.error('no test files matched'); process.exit(1); }
- const results = [];
- for (const f of files) {
- const r = await runOne(f);
- results.push(r);
- console.log(`${r.ok ? '✓' : '✗'} ${r.file.padEnd(32)} ${String(r.ms).padStart(6)}ms`);
- }
- const failed = results.filter((r) => !r.ok);
- for (const r of failed) {
- console.log(`\n── ${r.file} (exit ${r.code}) ──`);
- console.log(r.output.split('\n').slice(-60).join('\n'));
- }
- console.log(`\n${results.length - failed.length}/${results.length} test files passed`);
- process.exit(failed.length ? 1 : 0);
-})();
+require('openvibe-shared/test-runner').main({ dir: __dirname, timeoutMs: 60000, pad: 32, parallel: 1 });
@@ -106,7 +106,7 @@ t.test('every workflow has compiling, versioned input and output schemas and an
@@ -106,7 +106,7 @@ t.test('every workflow has compiling, versioned input and output schemas and an
t.test('every workflow runs end to end on the stub and returns schema-valid output', async () => {
t.test('every workflow runs end to end on the stub and returns schema-valid output', async () => {
for (const w of h.registry.listWorkflows()) {
for (const w of h.registry.listWorkflows()) {
- if (w.key === 'media.analyze' && !HAS_FFMPEG) continue; // test/media-analysis.test.js says so
+ if (w.key === 'media.analyze' && !HAS_FFMPEG) { console.log('workflow media.analyze end to end: skipped (no ffprobe)'); continue; }
const r = await request(h.base, 'POST', '/api/v1/runs?wait=10000', { tok, body: { workflow: w.key, input: EXAMPLES[w.key] } });
const r = await request(h.base, 'POST', '/api/v1/runs?wait=10000', { tok, body: { workflow: w.key, input: EXAMPLES[w.key] } });
assert.strictEqual(r.status, 201, `${w.key}: ${r.status} ${r.text.slice(0, 300)}`);
assert.strictEqual(r.status, 201, `${w.key}: ${r.status} ${r.text.slice(0, 300)}`);
const run = r.body.run;
const run = r.body.run;