diff --git a/lib/diagnostics-collectors.js b/lib/diagnostics-collectors.js index bce9e38..01d5bea 100644 --- a/lib/diagnostics-collectors.js +++ b/lib/diagnostics-collectors.js @@ -19,6 +19,15 @@ function createCollectors(deps) { try { return support.collectSecretValues(loadConfig()); } catch { return []; } } + function _currentHistory() { + if (typeof loadHistory === 'function') { + const history = loadHistory(); + return Array.isArray(history) ? history : []; + } + const cfg = loadConfig(); + return Array.isArray(cfg && cfg.history) ? cfg.history : []; + } + function _deepRedact(value, secrets) { return support.valueScrub(value, secrets || _secrets()); } @@ -162,15 +171,18 @@ function createCollectors(deps) { return { errors, byCategory }; } - function listErrors(args) { + function _listErrors(args, history) { const a = args || {}; - const cfg = loadConfig(); - const { errors, byCategory } = _historyErrors(cfg.history, a); + const { errors, byCategory } = _historyErrors(history, a); const limit = Math.min(Math.max(Number(a.limit) || 100, 1), 1000); const window = Number.isFinite(a.sinceMs) ? `since ${new Date(a.sinceMs).toISOString()}` : 'all history'; return { window, total: errors.length, byCategory, errors: errors.slice(-limit) }; } + function listErrors(args) { + return _listErrors(args, _currentHistory()); + } + function getQueueState(args) { const a = args || {}; const cfg = loadConfig(); @@ -200,11 +212,8 @@ function createCollectors(deps) { return result; } - function getHistory(args) { + function _getHistory(args, history) { const a = args || {}; - const history = typeof loadHistory === 'function' - ? (loadHistory() || []) - : (Array.isArray(loadConfig().history) ? loadConfig().history : []); const limit = Math.min(Math.max(Number(a.limit) || 20, 1), 200); const perHoster = stats.summarizePerHoster(history); const recent = [...history].slice(-limit).reverse(); @@ -227,6 +236,10 @@ function createCollectors(deps) { return { totalBatches: history.length, returned: batches.length, perHoster, batches }; } + function getHistory(args) { + return _getHistory(args, _currentHistory()); + } + function getRotationState() { const cfg = loadConfig(); return { rotationCursors: _deepRedact(cfg.rotationCursors || {}) }; @@ -246,9 +259,10 @@ function createCollectors(deps) { const a = args || {}; const errorLimit = Math.min(Math.max(Number(a.errorLimit) || 20, 1), 200); const errArgs = Number.isFinite(a.errorSinceMs) ? { sinceMs: a.errorSinceMs, limit: errorLimit } : { limit: errorLimit }; - const errors = listErrors(errArgs); + const currentHistory = _currentHistory(); + const errors = _listErrors(errArgs, currentHistory); const queue = getQueueState({ includeJobs: false }); - const history = getHistory({ limit: 5 }); + const history = _getHistory({ limit: 5 }, currentHistory); const warnings = []; if (queue.source === 'persisted' && queue.stale) warnings.push('queue state is from the persisted snapshot (may lag live state; UploadManager not introspected in this build).'); if (errors.total > 0) warnings.push(`${errors.total} non-success result(s) in the error window.`); diff --git a/tests/diagnostics-collectors.test.js b/tests/diagnostics-collectors.test.js index b146f67..e115e4b 100644 --- a/tests/diagnostics-collectors.test.js +++ b/tests/diagnostics-collectors.test.js @@ -86,6 +86,62 @@ test('getHistory falls back to loadConfig().history when loadHistory is absent ( assert.equal(c.getHistory({ limit: 10 }).totalBatches, 1, 'legacy path reads load().history when loadHistory not injected'); }); +test('getHistory, listErrors and serverHealth share loadHistory after migration', () => { + let historyRevision = 0; + const c = createCollectors({ + loadConfig: () => ({ hosters: {}, globalSettings: {}, history: [] }), + loadHistory: () => { + historyRevision++; + const timestamp = `2026-01-${String(historyRevision).padStart(2, '0')}T00:00:00.000Z`; + return [{ timestamp, files: [{ name: `failed-${historyRevision}.mkv`, results: [{ hoster: 'voe.sx', status: 'error', error: 'Not video file format' }] }] }]; + }, + getAllLogPaths: () => ({ logDir: os.tmpdir() }), + support, stats, + appInfo: () => ({}), systemInfo: () => ({}), agentInfo: () => ({}) + }); + const health = c.serverHealth({ errorLimit: 5 }); + const history = c.getHistory({ limit: 5 }); + const errors = c.listErrors({ limit: 5 }); + assert.deepEqual({ + historyBatches: history.totalBatches, + listedErrors: errors.total, + healthBatches: health.recentBatches.length, + healthErrors: health.errors.total + }, { + historyBatches: 1, + listedErrors: 1, + healthBatches: 1, + healthErrors: 1 + }); + assert.equal(health.recentBatches[0].timestamp, health.errors.errors[0].ts, 'serverHealth must summarize one history snapshot'); +}); + +test('history commands fail closed instead of reading stale config history', () => { + const c = createCollectors({ + loadConfig: () => ({ + hosters: {}, + globalSettings: {}, + history: [{ timestamp: '2025-12-31T00:00:00.000Z', files: [{ name: 'stale.mkv', results: [{ hoster: 'voe.sx', status: 'error', error: 'stale error' }] }] }] + }), + loadHistory: () => null, + getAllLogPaths: () => ({ logDir: os.tmpdir() }), + support, stats, + appInfo: () => ({}), systemInfo: () => ({}), agentInfo: () => ({}) + }); + const health = c.serverHealth({ errorLimit: 5 }); + assert.deepEqual({ + historyBatches: c.getHistory({ limit: 5 }).totalBatches, + listedErrors: c.listErrors({ limit: 5 }).total, + healthBatches: health.recentBatches.length, + healthErrors: health.errors.total + }, { + historyBatches: 0, + listedErrors: 0, + healthBatches: 0, + healthErrors: 0 + }); +}); + test('readLog redacts a planted token and a Bearer line; doodstream is NOT readable; unknown name rejected', () => { const { collectors, dir, paths } = makeFixture(); const dbg = collectors.readLog({ name: 'debug', tailKb: 64 });