openvibers/openrestream Commits
OpenRe's /api/ready is in the openvibe-shared/ready shape (roadmap WS-Q task 7, Shared v1.24.0). It answered { status, checks: { db: true, key: true }, … }, an ad-hoc body Network's registry could only read by HTTP status, so the status page showed OpenRe up (basis ready-legacy) on a 200 alone; since Network ac71bb4 it shows degraded for that reason. Now createReadiness with required db and network_key (as before: 503 without them) and optional workers (a ready rtmp-ingest and restream worker), coordinator (a valid lease) and events_relay, so a worker generation rolling over degrades the API, which keeps serving reads, rather than failing it. A restore drill's relay is skipped ("restore drill: never publishes"), never ok. The body keeps mode, store, workers, coordinator and events for scripts/cutover-preflight.js, which already requires status ready. openvibe-shared is pinned at v1.24.0 (from v1.22.0); README and STATUS.json name it. Tests: api (shape, required flags, no workers → degraded, preflight fields kept) and drill-mode (relay skipped). npm test (Node 22) 18/18.
Changes
@@@ -32,7 +32,7 @@ The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live- OpenVibe.Network (JWKS for service tokens and user JWTs; OAuth client `openre`; client-credentials tokens for Events/Media)- OpenVibe.Events (event relay; optional: rows wait in the outbox)- OpenVibe.Media (recording requests; optional: sessions work without it)--- OpenVibe.Contracts v0.49.0 (ids, problem+json, capability checks), OpenVibe.SDK v0.12.0 (outbox, token client, per-actor limits), OpenVibe.Shared v1.22.0 (chrome), pinned by release tarball++- OpenVibe.Contracts v0.49.0 (ids, problem+json, capability checks), OpenVibe.SDK v0.12.0 (outbox, token client, per-actor limits), OpenVibe.Shared v1.24.0 (chrome, readiness), pinned by release tarball- OpenVibe.Live (`live.lineage.resolve`: which channel a Live-linked stream belongs to)- node-media-server 2.7.4 (the RTMP session code Live runs) and the system ffmpeg@@@ -8,7 +8,7 @@"port": 4500,"rtmpPort": 1936,"domain": "openre.stream",-- "contracts": "openvibe-contracts v0.49.0 (openvibe-sdk v0.12.0, openvibe-shared v1.22.0)",++ "contracts": "openvibe-contracts v0.49.0 (openvibe-sdk v0.12.0, openvibe-shared v1.24.0)","protocols": {"rtmp": "ingest ported (openre-rtmp-ingest)","restream": "RTMP source to RTMP/RTMPS/SRT destinations ported (openre-restream-worker)",@@@ -16,7 +16,7 @@"node-media-server": "2.7.4","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.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"},"engines": {"node": ">=22.12.0"@@@ -1140,9 +1140,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 @@"node-media-server": "2.7.4","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.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"},"engines": {"node": ">=22.12.0"@@@ -10,6 +10,8 @@ const { createUiRouter } = require('./ui/routes');const pkg = require('../package.json');const SESSION_FLV_RE = /^(ses_[0-9A-HJKMNP-TV-Z]{26})\.flv$/;++// The worker kinds a switched slot needs (scripts/cutover-preflight.js checks the same two).++const WORKER_KINDS = Object.freeze(['rtmp-ingest', 'restream']);function createApp({ rt, auth, keys, log = console, fetchImpl }) {const { config, store, events, db } = rt;@@@ -56,30 +58,56 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {});release.mount(app);-- // Ready = the database answers and the Network key is loaded. Worker and coordinator state is-- // reported, not required: the API serves reads while a worker generation rolls over.-- app.get('/api/ready', (_req, res) => {-- let dbOk = false;-- try { dbOk = db.prepare('SELECT 1 AS ok').get().ok === 1; } catch { dbOk = false; }-- const checks = { db: dbOk, key: keys.loaded() };-- const ready = Object.values(checks).every(Boolean);-- let workers = [];-- let coordinator = null;-- try {-- workers = store.workers.alive().map(w => ({ kind: w.kind, generation: w.generation, state: w.state, heartbeat_age_ms: Date.now() - w.heartbeat_at }));-- const l = db.prepare("SELECT holder, expires_at FROM leases WHERE name = 'coordinator'").get();-- coordinator = l ? { holder: l.holder, lease_valid: l.expires_at > Date.now() } : null;-- } catch { /* reported as empty */ }-- res.status(ready ? 200 : 503).json({-- status: ready ? 'ready' : 'not_ready',-- ...(config.drill ? { mode: 'drill' } : {}),-- checks,-- store: { engine: 'sqlite', path_configured: Boolean(config.dbPath) },-- workers,-- coordinator,-- events: dbOk ? events.status() : null,-- });++ // Readiness in the openvibe-shared/ready shape (roadmap WS-Q task 7: the registry reads nothing else as green).++ // Required: the database answers and the Network key is loaded. Optional: a ready worker of each kind, a valid++ // coordinator lease and the event relay, so a worker generation rolling over degrades the API, which keeps++ // serving reads, rather than failing it. The worker list, the lease and the outbox stay in the body for++ // scripts/cutover-preflight.js; a restore drill never publishes, so its relay check is skipped, never ok.++ const { createReadiness, skip } = require('openvibe-shared/ready');++ const workerView = () => store.workers.alive().map(w => ({ kind: w.kind, generation: w.generation, state: w.state, heartbeat_age_ms: Date.now() - w.heartbeat_at }));++ const leaseView = () => {++ const l = db.prepare("SELECT holder, expires_at FROM leases WHERE name = 'coordinator'").get();++ return l ? { holder: l.holder, lease_valid: l.expires_at > Date.now() } : null;++ };++ const readiness = createReadiness({++ service: 'openre',++ release: release.release,++ checks: [++ { name: 'db', required: true, description: 'SQLite answers a query', check: () => db.prepare('SELECT 1 AS ok').get().ok === 1 },++ { name: 'network_key', required: true, description: 'OpenVibe.Network RS256 key (sign-in and service tokens)', check: () => keys.loaded() || 'Network public key not loaded yet' },++ {++ name: 'workers', required: false, description: 'a ready rtmp-ingest and restream worker',++ check: () => {++ const alive = workerView();++ const missing = WORKER_KINDS.filter(k => !alive.some(w => w.kind === k && w.state === 'ready'));++ return missing.length ? `no ready ${missing.join(' or ')} worker` : { ok: true, detail: { ready: alive.filter(w => w.state === 'ready').map(w => `${w.kind}#${w.generation}`) } };++ },++ },++ { name: 'coordinator', required: false, description: 'the coordinator holds a valid lease', check: () => { const l = leaseView(); return (l && l.lease_valid) || 'no valid coordinator lease'; } },++ {++ name: 'events_relay', required: false, description: 'durable events to OpenVibe.Events',++ check: () => {++ const st = events.status();++ if (!st.configured) return config.drill ? skip('restore drill: never publishes') : 'relay off (EVENTS_URL or OV_OAUTH_CLIENT_SECRET unset): openre.* events wait in the outbox';++ return { ok: true, detail: { pending: st.pending, rejected: st.rejected } };++ },++ },++ ],++ details: (body) => {++ let workers = [];++ let coordinator = null;++ try { workers = workerView(); coordinator = leaseView(); } catch { /* reported as empty */ }++ const dbOk = body.checks.db && body.checks.db.status === 'ok';++ return {++ ...(config.drill ? { mode: 'drill' } : {}),++ store: { engine: 'sqlite', path_configured: Boolean(config.dbPath) },++ workers,++ coordinator,++ events: dbOk ? events.status() : null,++ };++ },});++ app.get('/api/ready', readiness.handler);const apiJson = express.json({ limit: '256kb', type: ['application/json', 'application/*+json'] });const v1 = createV1Router({ rt, auth });@@@ -15,8 +15,15 @@ t('boot', async () => {api = await bootApi();const ready = await request(api.base, 'GET', '/api/ready');assert.strictEqual(ready.status, 200);-- assert.strictEqual(ready.body.checks.db, true);-- assert.strictEqual(ready.body.checks.key, true);++ // The openvibe-shared/ready shape (WS-Q task 7): required db and Network key; no workers in this test, so the++ // optional workers check degrades the API rather than failing it.++ assert.strictEqual(ready.body.service, 'openre');++ assert.strictEqual(ready.body.checks.db.status, 'ok');++ assert.strictEqual(ready.body.checks.network_key.status, 'ok');++ assert.deepStrictEqual([ready.body.checks.db.required, ready.body.checks.network_key.required, ready.body.checks.workers.required], [true, true, false]);++ assert.ok(ready.body.degraded.includes('workers'));++ assert.strictEqual(ready.body.status, 'degraded');++ assert.ok(Array.isArray(ready.body.workers) && 'coordinator' in ready.body && 'store' in ready.body, 'the preflight fields stay');});t('anonymous and ungranted callers are refused with problem+json', async () => {@@@ -82,6 +82,7 @@ t('openre-api in a drill: reads work and match a normal instance; writes, /play/assert.strictEqual(ready.status, 200);assert.strictEqual(ready.body.mode, 'drill');assert.strictEqual(ready.body.events.configured, false);++ assert.deepStrictEqual([ready.body.checks.events_relay.status, ready.body.checks.events_relay.reason], ['skipped', 'restore drill: never publishes'], 'a drill\'s relay is skipped, never ok');const h = await request(drill.base, 'GET', '/api/health');assert.strictEqual(h.text, health.text, '/api/health is byte-identical (ovhost compares it)');const l = await request(drill.base, 'GET', '/api/v1/streams', { token: reader });
@@ -32,7 +32,7 @@ The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live
@@ -32,7 +32,7 @@ The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live
- OpenVibe.Network (JWKS for service tokens and user JWTs; OAuth client `openre`; client-credentials tokens for Events/Media)
- OpenVibe.Network (JWKS for service tokens and user JWTs; OAuth client `openre`; client-credentials tokens for Events/Media)
- OpenVibe.Events (event relay; optional: rows wait in the outbox)
- OpenVibe.Events (event relay; optional: rows wait in the outbox)
- OpenVibe.Media (recording requests; optional: sessions work without it)
- OpenVibe.Media (recording requests; optional: sessions work without it)
-- OpenVibe.Contracts v0.49.0 (ids, problem+json, capability checks), OpenVibe.SDK v0.12.0 (outbox, token client, per-actor limits), OpenVibe.Shared v1.22.0 (chrome), pinned by release tarball
+- OpenVibe.Contracts v0.49.0 (ids, problem+json, capability checks), OpenVibe.SDK v0.12.0 (outbox, token client, per-actor limits), OpenVibe.Shared v1.24.0 (chrome, readiness), pinned by release tarball
- OpenVibe.Live (`live.lineage.resolve`: which channel a Live-linked stream belongs to)
- OpenVibe.Live (`live.lineage.resolve`: which channel a Live-linked stream belongs to)
- node-media-server 2.7.4 (the RTMP session code Live runs) and the system ffmpeg
- node-media-server 2.7.4 (the RTMP session code Live runs) and the system ffmpeg
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
"port": 4500,
"port": 4500,
"rtmpPort": 1936,
"rtmpPort": 1936,
"domain": "openre.stream",
"domain": "openre.stream",
- "contracts": "openvibe-contracts v0.49.0 (openvibe-sdk v0.12.0, openvibe-shared v1.22.0)",
+ "contracts": "openvibe-contracts v0.49.0 (openvibe-sdk v0.12.0, openvibe-shared v1.24.0)",
"protocols": {
"protocols": {
"rtmp": "ingest ported (openre-rtmp-ingest)",
"rtmp": "ingest ported (openre-rtmp-ingest)",
"restream": "RTMP source to RTMP/RTMPS/SRT destinations ported (openre-restream-worker)",
"restream": "RTMP source to RTMP/RTMPS/SRT destinations ported (openre-restream-worker)",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"node-media-server": "2.7.4",
"node-media-server": "2.7.4",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.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"
},
},
"engines": {
"engines": {
"node": ">=22.12.0"
"node": ">=22.12.0"
@@ -1140,9 +1140,9 @@
@@ -1140,9 +1140,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 @@
"node-media-server": "2.7.4",
"node-media-server": "2.7.4",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.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"
},
},
"engines": {
"engines": {
"node": ">=22.12.0"
"node": ">=22.12.0"
@@ -10,6 +10,8 @@ const { createUiRouter } = require('./ui/routes');
@@ -10,6 +10,8 @@ const { createUiRouter } = require('./ui/routes');
const pkg = require('../package.json');
const pkg = require('../package.json');
const SESSION_FLV_RE = /^(ses_[0-9A-HJKMNP-TV-Z]{26})\.flv$/;
const SESSION_FLV_RE = /^(ses_[0-9A-HJKMNP-TV-Z]{26})\.flv$/;
+// The worker kinds a switched slot needs (scripts/cutover-preflight.js checks the same two).
+const WORKER_KINDS = Object.freeze(['rtmp-ingest', 'restream']);
function createApp({ rt, auth, keys, log = console, fetchImpl }) {
function createApp({ rt, auth, keys, log = console, fetchImpl }) {
const { config, store, events, db } = rt;
const { config, store, events, db } = rt;
@@ -56,30 +58,56 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
@@ -56,30 +58,56 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
});
});
release.mount(app);
release.mount(app);
- // Ready = the database answers and the Network key is loaded. Worker and coordinator state is
- // reported, not required: the API serves reads while a worker generation rolls over.
- app.get('/api/ready', (_req, res) => {
- let dbOk = false;
- try { dbOk = db.prepare('SELECT 1 AS ok').get().ok === 1; } catch { dbOk = false; }
- const checks = { db: dbOk, key: keys.loaded() };
- const ready = Object.values(checks).every(Boolean);
- let workers = [];
- let coordinator = null;
- try {
- workers = store.workers.alive().map(w => ({ kind: w.kind, generation: w.generation, state: w.state, heartbeat_age_ms: Date.now() - w.heartbeat_at }));
- const l = db.prepare("SELECT holder, expires_at FROM leases WHERE name = 'coordinator'").get();
- coordinator = l ? { holder: l.holder, lease_valid: l.expires_at > Date.now() } : null;
- } catch { /* reported as empty */ }
- res.status(ready ? 200 : 503).json({
- status: ready ? 'ready' : 'not_ready',
- ...(config.drill ? { mode: 'drill' } : {}),
- checks,
- store: { engine: 'sqlite', path_configured: Boolean(config.dbPath) },
- workers,
- coordinator,
- events: dbOk ? events.status() : null,
- });
+ // Readiness in the openvibe-shared/ready shape (roadmap WS-Q task 7: the registry reads nothing else as green).
+ // Required: the database answers and the Network key is loaded. Optional: a ready worker of each kind, a valid
+ // coordinator lease and the event relay, so a worker generation rolling over degrades the API, which keeps
+ // serving reads, rather than failing it. The worker list, the lease and the outbox stay in the body for
+ // scripts/cutover-preflight.js; a restore drill never publishes, so its relay check is skipped, never ok.
+ const { createReadiness, skip } = require('openvibe-shared/ready');
+ const workerView = () => store.workers.alive().map(w => ({ kind: w.kind, generation: w.generation, state: w.state, heartbeat_age_ms: Date.now() - w.heartbeat_at }));
+ const leaseView = () => {
+ const l = db.prepare("SELECT holder, expires_at FROM leases WHERE name = 'coordinator'").get();
+ return l ? { holder: l.holder, lease_valid: l.expires_at > Date.now() } : null;
+ };
+ const readiness = createReadiness({
+ service: 'openre',
+ release: release.release,
+ checks: [
+ { name: 'db', required: true, description: 'SQLite answers a query', check: () => db.prepare('SELECT 1 AS ok').get().ok === 1 },
+ { name: 'network_key', required: true, description: 'OpenVibe.Network RS256 key (sign-in and service tokens)', check: () => keys.loaded() || 'Network public key not loaded yet' },
+ {
+ name: 'workers', required: false, description: 'a ready rtmp-ingest and restream worker',
+ check: () => {
+ const alive = workerView();
+ const missing = WORKER_KINDS.filter(k => !alive.some(w => w.kind === k && w.state === 'ready'));
+ return missing.length ? `no ready ${missing.join(' or ')} worker` : { ok: true, detail: { ready: alive.filter(w => w.state === 'ready').map(w => `${w.kind}#${w.generation}`) } };
+ },
+ },
+ { name: 'coordinator', required: false, description: 'the coordinator holds a valid lease', check: () => { const l = leaseView(); return (l && l.lease_valid) || 'no valid coordinator lease'; } },
+ {
+ name: 'events_relay', required: false, description: 'durable events to OpenVibe.Events',
+ check: () => {
+ const st = events.status();
+ if (!st.configured) return config.drill ? skip('restore drill: never publishes') : 'relay off (EVENTS_URL or OV_OAUTH_CLIENT_SECRET unset): openre.* events wait in the outbox';
+ return { ok: true, detail: { pending: st.pending, rejected: st.rejected } };
+ },
+ },
+ ],
+ details: (body) => {
+ let workers = [];
+ let coordinator = null;
+ try { workers = workerView(); coordinator = leaseView(); } catch { /* reported as empty */ }
+ const dbOk = body.checks.db && body.checks.db.status === 'ok';
+ return {
+ ...(config.drill ? { mode: 'drill' } : {}),
+ store: { engine: 'sqlite', path_configured: Boolean(config.dbPath) },
+ workers,
+ coordinator,
+ events: dbOk ? events.status() : null,
+ };
+ },
});
});
+ app.get('/api/ready', readiness.handler);
const apiJson = express.json({ limit: '256kb', type: ['application/json', 'application/*+json'] });
const apiJson = express.json({ limit: '256kb', type: ['application/json', 'application/*+json'] });
const v1 = createV1Router({ rt, auth });
const v1 = createV1Router({ rt, auth });
@@ -15,8 +15,15 @@ t('boot', async () => {
@@ -15,8 +15,15 @@ t('boot', async () => {
api = await bootApi();
api = await bootApi();
const ready = await request(api.base, 'GET', '/api/ready');
const ready = await request(api.base, 'GET', '/api/ready');
assert.strictEqual(ready.status, 200);
assert.strictEqual(ready.status, 200);
- assert.strictEqual(ready.body.checks.db, true);
- assert.strictEqual(ready.body.checks.key, true);
+ // The openvibe-shared/ready shape (WS-Q task 7): required db and Network key; no workers in this test, so the
+ // optional workers check degrades the API rather than failing it.
+ assert.strictEqual(ready.body.service, 'openre');
+ assert.strictEqual(ready.body.checks.db.status, 'ok');
+ assert.strictEqual(ready.body.checks.network_key.status, 'ok');
+ assert.deepStrictEqual([ready.body.checks.db.required, ready.body.checks.network_key.required, ready.body.checks.workers.required], [true, true, false]);
+ assert.ok(ready.body.degraded.includes('workers'));
+ assert.strictEqual(ready.body.status, 'degraded');
+ assert.ok(Array.isArray(ready.body.workers) && 'coordinator' in ready.body && 'store' in ready.body, 'the preflight fields stay');
});
});
t('anonymous and ungranted callers are refused with problem+json', async () => {
t('anonymous and ungranted callers are refused with problem+json', async () => {
@@ -82,6 +82,7 @@ t('openre-api in a drill: reads work and match a normal instance; writes, /play/
@@ -82,6 +82,7 @@ t('openre-api in a drill: reads work and match a normal instance; writes, /play/
assert.strictEqual(ready.status, 200);
assert.strictEqual(ready.status, 200);
assert.strictEqual(ready.body.mode, 'drill');
assert.strictEqual(ready.body.mode, 'drill');
assert.strictEqual(ready.body.events.configured, false);
assert.strictEqual(ready.body.events.configured, false);
+ assert.deepStrictEqual([ready.body.checks.events_relay.status, ready.body.checks.events_relay.reason], ['skipped', 'restore drill: never publishes'], 'a drill\'s relay is skipped, never ok');
const h = await request(drill.base, 'GET', '/api/health');
const h = await request(drill.base, 'GET', '/api/health');
assert.strictEqual(h.text, health.text, '/api/health is byte-identical (ovhost compares it)');
assert.strictEqual(h.text, health.text, '/api/health is byte-identical (ovhost compares it)');
const l = await request(drill.base, 'GET', '/api/v1/streams', { token: reader });
const l = await request(drill.base, 'GET', '/api/v1/streams', { token: reader });