fix: unify diagnostics history access

Resolve diagnostic history through the injected loadHistory dependency whenever it is available, retaining config history only for legacy collectors without a dedicated reader.

Build server_health error and batch summaries from one validated snapshot so migrated installations cannot combine current batches with stale errors or parse the external history twice.

Add focused regressions for migrated history consistency and fail-closed handling of invalid dedicated history results.
This commit is contained in:
Sucukdeluxe
2026-08-13 22:28:26 +02:00
parent a4d0854e76
commit acb43d982e
2 changed files with 79 additions and 9 deletions
+23 -9
View File
@@ -19,6 +19,15 @@ function createCollectors(deps) {
try { return support.collectSecretValues(loadConfig()); } catch { return []; } 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) { function _deepRedact(value, secrets) {
return support.valueScrub(value, secrets || _secrets()); return support.valueScrub(value, secrets || _secrets());
} }
@@ -162,15 +171,18 @@ function createCollectors(deps) {
return { errors, byCategory }; return { errors, byCategory };
} }
function listErrors(args) { function _listErrors(args, history) {
const a = args || {}; const a = args || {};
const cfg = loadConfig(); const { errors, byCategory } = _historyErrors(history, a);
const { errors, byCategory } = _historyErrors(cfg.history, a);
const limit = Math.min(Math.max(Number(a.limit) || 100, 1), 1000); 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'; const window = Number.isFinite(a.sinceMs) ? `since ${new Date(a.sinceMs).toISOString()}` : 'all history';
return { window, total: errors.length, byCategory, errors: errors.slice(-limit) }; return { window, total: errors.length, byCategory, errors: errors.slice(-limit) };
} }
function listErrors(args) {
return _listErrors(args, _currentHistory());
}
function getQueueState(args) { function getQueueState(args) {
const a = args || {}; const a = args || {};
const cfg = loadConfig(); const cfg = loadConfig();
@@ -200,11 +212,8 @@ function createCollectors(deps) {
return result; return result;
} }
function getHistory(args) { function _getHistory(args, history) {
const a = args || {}; 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 limit = Math.min(Math.max(Number(a.limit) || 20, 1), 200);
const perHoster = stats.summarizePerHoster(history); const perHoster = stats.summarizePerHoster(history);
const recent = [...history].slice(-limit).reverse(); const recent = [...history].slice(-limit).reverse();
@@ -227,6 +236,10 @@ function createCollectors(deps) {
return { totalBatches: history.length, returned: batches.length, perHoster, batches }; return { totalBatches: history.length, returned: batches.length, perHoster, batches };
} }
function getHistory(args) {
return _getHistory(args, _currentHistory());
}
function getRotationState() { function getRotationState() {
const cfg = loadConfig(); const cfg = loadConfig();
return { rotationCursors: _deepRedact(cfg.rotationCursors || {}) }; return { rotationCursors: _deepRedact(cfg.rotationCursors || {}) };
@@ -246,9 +259,10 @@ function createCollectors(deps) {
const a = args || {}; const a = args || {};
const errorLimit = Math.min(Math.max(Number(a.errorLimit) || 20, 1), 200); 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 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 queue = getQueueState({ includeJobs: false });
const history = getHistory({ limit: 5 }); const history = _getHistory({ limit: 5 }, currentHistory);
const warnings = []; 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 (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.`); if (errors.total > 0) warnings.push(`${errors.total} non-success result(s) in the error window.`);
+56
View File
@@ -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'); 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', () => { test('readLog redacts a planted token and a Bearer line; doodstream is NOT readable; unknown name rejected', () => {
const { collectors, dir, paths } = makeFixture(); const { collectors, dir, paths } = makeFixture();
const dbg = collectors.readLog({ name: 'debug', tailKb: 64 }); const dbg = collectors.readLog({ name: 'debug', tailKb: 64 });