openvibers/openvibe.network Commits

No fake green in Network's readiness and in the registry's status (roadmap WS-Q task 7; Shared v1.24.0). Production's /api/ready reported the Discord bot check "ok" with the detail "not configured"; it now returns skip('not configured'), so it reads skipped with its reason and never ok (an optional skip leaves the verified service ready). The registry (/api/v1/status, the registry's runtime rows) called three things "up" that verified nothing: a readiness body not in the openvibe-shared/ready shape (OpenRe's, basis ready-legacy: HTTP 200 alone), a liveness-only answer (basis health), and a shared-shape body whose checks all skipped or that had none. Each is now degraded with the reason; up needs a shared-shape body that says ready, degrades nothing and has at least one check with status ok, whatever Shared version wrote it. The row's ready summary keeps a skipped check as skipped (with its reason) instead of fail, and lists skipped. openvibe-shared is pinned at v1.24.0 (from v1.22.0). Tests: observability (no bot token: skipped, still ready), status (liveness only and the ad-hoc body are degraded; skip beside a pass stays up; all skipped or none is degraded), registry-ecosystem (its stand-in service now answers a real readiness body). npm test passes.

OP OpenVibers committed 28 Sept 2026 · ac71bb4d82 · 7 files, +56 −16


Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 2bca78cd..ec566f51 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"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.12.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.24.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
}
@@@ -1490,9 +1490,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.24.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.24.0",
++ "integrity": "sha512-VPzafrc07Pw0oqmXioOlDRtlX5xu82jNLBYkVYxXvhjkILMpWsyjExItIRt5Y6l2st0ozHm7bDQFTJpKEShMEA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex a39fde1a..f8075ecc 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"multer": "^1.4.5-lts.1",
"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.12.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.24.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
}
ddiff --git a/server/observability.js b/server/observability.js
iindex 4516bad2..0a2c5f67 100644
---- a/server/observability.js
++++ b/server/observability.js
@@@ -11,7 +11,7 @@
*/
const jwt = require('jsonwebtoken');
const metrics = require('openvibe-shared/metrics');
--const { createReadiness } = require('openvibe-shared/ready');
++const { createReadiness, skip } = require('openvibe-shared/ready');
const registry = metrics.createRegistry();
@@@ -56,7 +56,7 @@ function principalDenied({ req, code }) {
* signing_key (required in production) an RS256 keypair is loaded and signs a token its own public
* key verifies — the thing every service's offline verification depends on
* registry_poll (optional) the ecosystem health poll has run recently (status page freshness)
-- * discord_bot (optional, only when a bot token is configured) the bot is connected
++ * discord_bot (optional) the bot is connected; skipped (never ok) while no bot token is configured
*/
function createNetworkReadiness({ db, getKeys, release, ecosystem = null, discordService = null, production = process.env.NODE_ENV === 'production', pollMs = 60000 }) {
const checks = [
@@@ -90,7 +90,7 @@ function createNetworkReadiness({ db, getKeys, release, ecosystem = null, discor
check: () => {
let configured = false;
try { configured = !!discordService._getSetting('discord_bot_token'); } catch { configured = false; }
-- if (!configured) return { detail: 'not configured' };
++ if (!configured) return skip('not configured'); // verified nothing: skipped, never ok (WS-Q task 7)
return discordService.isReady() || 'bot token configured but the bot is not connected';
},
});
ddiff --git a/server/registry/ecosystem.js b/server/registry/ecosystem.js
iindex 21268b20..da4f203b 100644
---- a/server/registry/ecosystem.js
++++ b/server/registry/ecosystem.js
@@@ -49,13 +49,20 @@ const POLL_MS = 60 * 1000;
const READY_PATHS = {};
const HEALTH_PATHS = {};
--/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */
++/**
++ * Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). No fake green (roadmap
++ * WS-Q task 7): `up` needs a shared-shape body that says ready, degrades nothing and has at least one check that
++ * really passed (status ok; skipped never counts). An ad-hoc body is `degraded` at best, since none of its checks
++ * can be read here, and a body whose checks all skipped (or none) is `degraded`, whatever Shared version wrote it.
++ */
function statusFromReady(res, body) {
const shared = body && typeof body === 'object' && typeof body.ready === 'boolean' && body.checks && typeof body.checks === 'object';
-- if (!shared) return { status: res.ok ? 'up' : 'down', basis: 'ready-legacy' };
++ if (!shared) return res.ok ? { status: 'degraded', basis: 'ready-legacy', reason: 'readiness is not in the openvibe-shared/ready shape: no check can be verified' } : { status: 'down', basis: 'ready-legacy' };
if (!body.ready) return { status: 'down', basis: 'ready' };
const degraded = Array.isArray(body.degraded) ? body.degraded : [];
-- return { status: body.status === 'degraded' || degraded.length ? 'degraded' : 'up', basis: 'ready' };
++ const verified = Object.values(body.checks).some((c) => c && typeof c === 'object' && c.status === 'ok');
++ if (!verified) return { status: 'degraded', basis: 'ready', reason: 'no readiness check passed (none, or all skipped)' };
++ return { status: body.status === 'ready' && !degraded.length ? 'up' : 'degraded', basis: 'ready' };
}
/** What a status row may repeat from a service's readiness body: names, states and times, no free-form detail. */
@@@ -65,14 +72,15 @@ function readySummary(body) {
for (const [name, c] of Object.entries(body.checks || {}).slice(0, 40)) {
if (!c || typeof c !== 'object') continue;
checks[String(name).slice(0, 64)] = {
-- status: c.status === 'ok' ? 'ok' : 'fail', required: c.required !== false,
++ status: c.status === 'ok' ? 'ok' : c.status === 'skipped' ? 'skipped' : 'fail', required: c.required !== false,
latency_ms: Number.isFinite(c.latency_ms) ? c.latency_ms : null,
checked_at: typeof c.checked_at === 'string' ? c.checked_at.slice(0, 40) : null,
...(c.error ? { error: String(c.error).slice(0, 200) } : {}),
++ ...(c.status === 'skipped' && c.reason ? { reason: String(c.reason).slice(0, 200) } : {}),
};
}
const names = (a) => (Array.isArray(a) ? a.map(x => String(x).slice(0, 64)).slice(0, 40) : []);
-- return { ready: body.ready, status: String(body.status || (body.ready ? 'ready' : 'not_ready')).slice(0, 20), checked_at: typeof body.checked_at === 'string' ? body.checked_at.slice(0, 40) : null, failed: names(body.failed), degraded: names(body.degraded), checks };
++ return { ready: body.ready, status: String(body.status || (body.ready ? 'ready' : 'not_ready')).slice(0, 20), checked_at: typeof body.checked_at === 'string' ? body.checked_at.slice(0, 40) : null, failed: names(body.failed), degraded: names(body.degraded), skipped: names(body.skipped), checks };
}
/**
@@@ -176,7 +184,8 @@ function createEcosystemRegistry({ issuer, internalOverrides = {}, fetchImpl = g
// No readiness endpoint (yet): liveness is all that can be said.
if (!healthPath) throw Object.assign(new Error('readiness endpoint answered 404'), { name: 'NoReadiness' });
got = await getJson(`${base}${healthPath}`);
-- entry = { status: got.res.ok ? 'up' : 'down', basis: 'health', reason: 'liveness only: no readiness endpoint answered', ready: null };
++ // Liveness says the process answers, not that it serves: never green (WS-Q task 7).
++ entry = { status: got.res.ok ? 'degraded' : 'down', basis: 'health', reason: 'liveness only: no readiness endpoint answered', ready: null };
}
entry.http_status = got.res.status;
entry.latency_ms = got.latency;
ddiff --git a/test/observability.test.js b/test/observability.test.js
iindex c59d8757..4c039802 100644
---- a/test/observability.test.js
++++ b/test/observability.test.js
@@@ -115,6 +115,17 @@ const server = http.createServer(app);
rd = await get('/api/ready'); body = JSON.parse(rd.text);
assert.strictEqual(body.status, 'ready');
++ // No bot token: the Discord check verified nothing, so it is skipped with the reason, never ok (WS-Q task 7),
++ // and an optional skip leaves the verified service ready.
++ const noBot = observability.createNetworkReadiness({
++ db, release: 'x', production: true, getKeys: () => keysNow, ecosystem: { lastPollAt: () => lastPoll },
++ discordService: { isReady: () => false, _getSetting: () => null },
++ });
++ body = await noBot.run();
++ assert.deepStrictEqual([body.checks.discord_bot.status, body.checks.discord_bot.reason], ['skipped', 'not configured']);
++ assert.deepStrictEqual(body.skipped, ['discord_bot']);
++ assert.strictEqual(body.status, 'ready');
++
// An ephemeral HS256 key in production is not ready: nothing else can verify its tokens.
const eph = crypto.randomBytes(32).toString('hex');
keysNow = { privateKey: eph, publicKey: eph };
ddiff --git a/test/registry-ecosystem.test.js b/test/registry-ecosystem.test.js
iindex a2a42664..00b0643a 100644
---- a/test/registry-ecosystem.test.js
++++ b/test/registry-ecosystem.test.js
@@@ -8,7 +8,13 @@ const contracts = require('openvibe-contracts');
const { createEcosystemRegistry } = require('../server/registry/ecosystem');
(async () => {
-- const up = http.createServer((req, res) => { res.statusCode = req.url === '/api/health' ? 200 : 404; res.end('{}'); });
++ // A running service answers readiness in the openvibe-shared/ready shape with a check that passed (liveness
++ // alone is never 'up': test/status.test.js).
++ const READY = JSON.stringify({ ready: true, status: 'ready', failed: [], degraded: [], skipped: [], checks: { db: { status: 'ok', required: true } } });
++ const up = http.createServer((req, res) => {
++ res.statusCode = req.url === '/api/health' || req.url === '/api/ready' ? 200 : 404;
++ res.end(req.url === '/api/ready' ? READY : '{}');
++ });
await new Promise(r => up.listen(0, '127.0.0.1', r));
const upUrl = `http://127.0.0.1:${up.address().port}`;
const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network', internalOverrides: { network: upUrl, live: 'http://127.0.0.1:1', media: upUrl } });
ddiff --git a/test/status.test.js b/test/status.test.js
iindex e64e0e06..e60c3689 100644
---- a/test/status.test.js
++++ b/test/status.test.js
@@@ -66,12 +66,26 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
assert.deepStrictEqual(by.media.ready.degraded, ['remote_b2']);
assert.strictEqual(by.media.ready.checks.remote_b2.error, 'timeout');
assert.strictEqual(by.community.status, 'up');
-- assert.strictEqual(by.tools.status, 'up');
++ assert.strictEqual(by.tools.status, 'degraded', 'liveness only is never green (WS-Q task 7)');
assert.strictEqual(by.tools.basis, 'health', 'no readiness endpoint: liveness only, and the row says so');
assert.strictEqual(by.tools.release, null);
assert.ok(by.tools.release_error);
assert.strictEqual(by.events.basis, 'ready-legacy');
++ assert.strictEqual(by.events.status, 'degraded', 'an ad-hoc readiness body: no check can be verified, so not green');
++ assert.match(by.events.reason, /not in the openvibe-shared\/ready shape/);
assert.strictEqual(by.live.status, 'down', 'unreachable is down');
++
++ // No fake green (WS-Q task 7): skipped checks never make a row green, whatever Shared version wrote the body.
++ const { statusFromReady, readySummary } = require('../server/registry/ecosystem');
++ const okRes = { ok: true };
++ const skippedCheck = { status: 'skipped', required: false, reason: 'not configured', latency_ms: 0, checked_at: '2026-09-28T00:00:00Z' };
++ const withSkip = { ready: true, status: 'ready', failed: [], degraded: [], skipped: ['discord_bot'], checks: { db: check('ok', true), discord_bot: skippedCheck } };
++ assert.strictEqual(statusFromReady(okRes, withSkip).status, 'up', 'an optional skip beside a passing check stays up');
++ assert.deepStrictEqual(readySummary(withSkip).checks.discord_bot, { status: 'skipped', required: false, latency_ms: 0, checked_at: '2026-09-28T00:00:00Z', reason: 'not configured' });
++ assert.deepStrictEqual(readySummary(withSkip).skipped, ['discord_bot']);
++ assert.strictEqual(statusFromReady(okRes, { ready: true, status: 'ready', failed: [], degraded: [], checks: { a: skippedCheck } }).status, 'degraded', 'all skipped: not green');
++ assert.strictEqual(statusFromReady(okRes, { ready: true, status: 'ready', failed: [], degraded: [], checks: {} }).status, 'degraded', 'no checks: not green');
++ assert.strictEqual(statusFromReady({ ok: false }, { status: 'x' }).status, 'down');
assert.strictEqual(by.realtime.status, 'not-running');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.contracts.status, 'not-running');