// Counter-derived chart regression: pauses must not become fake 0/s samples. // Real Cache / Transform chart functions run against simulated DOM and clock. import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const samplingSource = fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'); const panels = { cache: { path: 'src/admin/web/cache-panel.js', stop: ' function configure()', counters: (hit, miss) => ({ hits: hit, misses: miss, bypasses: 0 }), graph: 'cache-live-hit-line', rate: 'cache-live-hit-rate', keys: ['hits', 'misses', 'bypasses'], seconds: 5, }, transform: { path: 'src/admin/web/transform-panel.js', stop: ' function countersFor(data)', counters: (hit, miss) => ({ applied: hit, cache_hits: miss, skipped: 0, errors: 0 }), graph: 'transform-live-applied-line', rate: 'transform-live-applied-rate', keys: ['applied', 'cache_hits', 'skipped', 'errors'], seconds: 3, }, }; function fixture(name, config) { const source = fs.readFileSync(config.path, 'utf8'); const end = source.indexOf(config.stop); assert(end > 0, `unable to isolate ${name} live chart`); const documentListeners = new Map(), windowListeners = new Map(), nodes = new Map(); let now = 1000; const node = id => { if (!nodes.has(id)) nodes.set(id, { value: id.endsWith('-scope') ? '*' : '', textContent: '', attrs: {}, setAttribute(key, value) { this.attrs[key] = value; }, }); return nodes.get(id); }; const document = { hidden: false, addEventListener(name, listener) { documentListeners.set(name, listener); }, }; const window = { addEventListener(name, listener) { windowListeners.set(name, listener); }, rustProxyRefresh: { current: () => ({ overview_seconds: 2, cache_seconds: config.seconds }) }, }; const ctx = { document, window, $: node, performance: { now: () => now }, sparklinePoints: samples => samples.join(','), }; vm.createContext(ctx); vm.runInContext(samplingSource, ctx); vm.runInContext(source.slice(0, end) + '\nwindow.liveChartTest = { renderLiveChart, resetLiveChart, liveChartSamples };\n})();', ctx); const chart = window.liveChartTest; const line = () => node(config.graph).attrs.points; const rate = () => node(config.rate).textContent; const length = () => chart.liveChartSamples[config.keys[0]].length; const sample = (stamp, hits, misses) => { now = stamp; chart.renderLiveChart(config.counters(hits, misses)); }; return { ctx, chart, line, rate, length, sample, document, documentListeners, windowListeners }; } for (const [name, config] of Object.entries(panels)) { const f = fixture(name, config); f.sample(1000, 100, 50); // no deltas exist before the first observation assert.equal(f.length(), 0, `${name}: first reading is a baseline, not 0/s`); assert.equal(f.rate(), '-'); f.sample(1000 + config.seconds * 1000, 130, 65); assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`); assert.equal(f.length(), 1); f.sample(1000 + config.seconds * 2000, 130, 65); assert.equal(f.rate(), '0.0/s', `${name}: real idle interval is still zero`); assert.equal(f.length(), 2); const graphBefore = f.line(); const delayed = 1000 + config.seconds * 2000 + 120000; f.sample(delayed, 500, 200); assert.equal(f.rate(), '-', `${name}: long pause cannot be averaged into a misleading rate`); assert.equal(f.length(), 2, `${name}: do not append fake drop`); assert.equal(f.line(), graphBefore, `${name}: keep old chart path`); f.sample(delayed + config.seconds * 1000, 530, 215); assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`, `${name}: next fresh interval resumes`); assert.equal(f.length(), 3); f.document.hidden = true; f.documentListeners.get('visibilitychange')?.(); f.document.hidden = false; f.documentListeners.get('visibilitychange')?.(); const afterBackground = delayed + config.seconds * 1000 + 200; f.sample(afterBackground, 533, 219); assert.equal(f.length(), 3, `${name}: even a short background hide must not append a rate`); assert.equal(f.rate(), '-'); f.sample(afterBackground + config.seconds * 1000, 533, 219); assert.equal(f.rate(), '0.0/s', `${name}: genuine zero after resume`); assert.equal(f.length(), 4); f.windowListeners.get('pagehide')?.(); f.sample(afterBackground + config.seconds * 2000, 540, 220); assert.equal(f.length(), 4, `${name}: pagehide / BFCache must rebase`); f.sample(afterBackground + config.seconds * 3000, 550, 230); assert.equal(f.length(), 5); f.sample(afterBackground + config.seconds * 4000, 1, 1); assert.equal(f.length(), 5, `${name}: counters reset must not be interpreted as 0/s`); f.sample(afterBackground + config.seconds * 5000, 3, 3); assert.equal(f.length(), 6, `${name}: counter reset recovery`); } // All remaining rate charts share this exact continuity check, including // configured 30-second Cache refresh (not falsely treated as a pause). { const listeners = new Map(), document = { hidden: false, addEventListener(key, fn) { listeners.set(key, fn); } }; const context = { window: {}, document }; vm.createContext(context); vm.runInContext(samplingSource, context); const sample = vm.runInContext('rustProxyRateSampling', context); let baseline = sample.difference(null, [100], 1000, 30).next; let value = sample.difference(baseline, [130], 31000, 30); assert.equal(value.deltas[0], 30, '30-second chart interval is valid'); baseline = value.next; value = sample.difference(baseline, [200], 124000, 30); assert.equal(value.deltas, null, 'large delay is a gap'); document.hidden = true; listeners.get('visibilitychange')?.(); document.hidden = false; assert.equal(sample.continuous(value.next, 124001, 30), false, 'hide invalidates even a short elapsed period'); } console.log('PASS: Cache/Transform history survives hidden tabs, long pauses, BFCache and counter resets; real 0/s and configured cadence remain intact.');