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.
Changes
@@@ -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"@@@ -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"}@@@ -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, discorcheck: () => {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';},});@@@ -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;@@@ -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 };@@@ -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 } });@@@ -66,12 +66,26 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22assert.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');
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"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.12.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",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
}
}
@@ -1490,9 +1490,9 @@
@@ -1490,9 +1490,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.24.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.24.0",
+ "integrity": "sha512-VPzafrc07Pw0oqmXioOlDRtlX5xu82jNLBYkVYxXvhjkILMpWsyjExItIRt5Y6l2st0ozHm7bDQFTJpKEShMEA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"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.12.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",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
}
}
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
*/
*/
const jwt = require('jsonwebtoken');
const jwt = require('jsonwebtoken');
const metrics = require('openvibe-shared/metrics');
const metrics = require('openvibe-shared/metrics');
-const { createReadiness } = require('openvibe-shared/ready');
+const { createReadiness, skip } = require('openvibe-shared/ready');
const registry = metrics.createRegistry();
const registry = metrics.createRegistry();
@@ -56,7 +56,7 @@ function principalDenied({ req, code }) {
@@ -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
* 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
* key verifies — the thing every service's offline verification depends on
* registry_poll (optional) the ecosystem health poll has run recently (status page freshness)
* 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 }) {
function createNetworkReadiness({ db, getKeys, release, ecosystem = null, discordService = null, production = process.env.NODE_ENV === 'production', pollMs = 60000 }) {
const checks = [
const checks = [
@@ -90,7 +90,7 @@ function createNetworkReadiness({ db, getKeys, release, ecosystem = null, discor
@@ -90,7 +90,7 @@ function createNetworkReadiness({ db, getKeys, release, ecosystem = null, discor
check: () => {
check: () => {
let configured = false;
let configured = false;
try { configured = !!discordService._getSetting('discord_bot_token'); } catch { 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';
return discordService.isReady() || 'bot token configured but the bot is not connected';
},
},
});
});
@@ -49,13 +49,20 @@ const POLL_MS = 60 * 1000;
@@ -49,13 +49,20 @@ const POLL_MS = 60 * 1000;
const READY_PATHS = {};
const READY_PATHS = {};
const HEALTH_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) {
function statusFromReady(res, body) {
const shared = body && typeof body === 'object' && typeof body.ready === 'boolean' && body.checks && typeof body.checks === 'object';
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' };
if (!body.ready) return { status: 'down', basis: 'ready' };
const degraded = Array.isArray(body.degraded) ? body.degraded : [];
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. */
/** 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) {
@@ -65,14 +72,15 @@ function readySummary(body) {
for (const [name, c] of Object.entries(body.checks || {}).slice(0, 40)) {
for (const [name, c] of Object.entries(body.checks || {}).slice(0, 40)) {
if (!c || typeof c !== 'object') continue;
if (!c || typeof c !== 'object') continue;
checks[String(name).slice(0, 64)] = {
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,
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,
checked_at: typeof c.checked_at === 'string' ? c.checked_at.slice(0, 40) : null,
...(c.error ? { error: String(c.error).slice(0, 200) } : {}),
...(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) : []);
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
@@ -176,7 +184,8 @@ function createEcosystemRegistry({ issuer, internalOverrides = {}, fetchImpl = g
// No readiness endpoint (yet): liveness is all that can be said.
// No readiness endpoint (yet): liveness is all that can be said.
if (!healthPath) throw Object.assign(new Error('readiness endpoint answered 404'), { name: 'NoReadiness' });
if (!healthPath) throw Object.assign(new Error('readiness endpoint answered 404'), { name: 'NoReadiness' });
got = await getJson(`${base}${healthPath}`);
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.http_status = got.res.status;
entry.latency_ms = got.latency;
entry.latency_ms = got.latency;
@@ -115,6 +115,17 @@ const server = http.createServer(app);
@@ -115,6 +115,17 @@ const server = http.createServer(app);
rd = await get('/api/ready'); body = JSON.parse(rd.text);
rd = await get('/api/ready'); body = JSON.parse(rd.text);
assert.strictEqual(body.status, 'ready');
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.
// An ephemeral HS256 key in production is not ready: nothing else can verify its tokens.
const eph = crypto.randomBytes(32).toString('hex');
const eph = crypto.randomBytes(32).toString('hex');
keysNow = { privateKey: eph, publicKey: eph };
keysNow = { privateKey: eph, publicKey: eph };
@@ -8,7 +8,13 @@ const contracts = require('openvibe-contracts');
@@ -8,7 +8,13 @@ const contracts = require('openvibe-contracts');
const { createEcosystemRegistry } = require('../server/registry/ecosystem');
const { createEcosystemRegistry } = require('../server/registry/ecosystem');
(async () => {
(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));
await new Promise(r => up.listen(0, '127.0.0.1', r));
const upUrl = `http://127.0.0.1:${up.address().port}`;
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 } });
const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network', internalOverrides: { network: upUrl, live: 'http://127.0.0.1:1', media: upUrl } });
@@ -66,12 +66,26 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
@@ -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.deepStrictEqual(by.media.ready.degraded, ['remote_b2']);
assert.strictEqual(by.media.ready.checks.remote_b2.error, 'timeout');
assert.strictEqual(by.media.ready.checks.remote_b2.error, 'timeout');
assert.strictEqual(by.community.status, 'up');
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.basis, 'health', 'no readiness endpoint: liveness only, and the row says so');
assert.strictEqual(by.tools.release, null);
assert.strictEqual(by.tools.release, null);
assert.ok(by.tools.release_error);
assert.ok(by.tools.release_error);
assert.strictEqual(by.events.basis, 'ready-legacy');
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');
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.status, 'not-running');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.contracts.status, 'not-running');
assert.strictEqual(by.contracts.status, 'not-running');