'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { applyPulse, chapterInsight, compactRanges, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary, } = require('../reading-analytics'); const pulse = (session, seq, extra = {}) => ({ session, seq, activeMs: 0, loadingMs: 0, inputMs: { mouse: 0, touch: 0, keyboard: 0 }, seen: [], fastBursts: 0, rejectedPageViews: 0, maxPps: 0, ...extra, }); test('page ranges remain compact and deterministic', () => { assert.deepEqual(compactRanges([8, 2, 3, 3, 4, 9, 12]), [[2, 4], [8, 9], [12, 12]]); }); test('reading pulses are idempotent and server-clamped', () => { const record = emptyRecord(), options = { version: 'source-a', pageCount: 4, eligible: [0, 1, 2, 3], now: 10000 }; applyPulse(record, pulse('session-one', 1, { activeMs: 9800, inputMs: { touch: 9800 } }), options); assert.equal(record.totalActiveMs, 9800); assert.equal(record.sessions, 1); assert.equal(record.inputMs.touch, 9800); const duplicate = applyPulse(record, pulse('session-one', 1, { activeMs: 9800 }), { ...options, now: 20000 }); assert.equal(duplicate.duplicate, true); assert.equal(record.totalActiveMs, 9800); applyPulse(record, pulse('session-one', 2, { activeMs: 50000, loadingMs: 50000, inputMs: { mouse: 50000 } }), { ...options, now: 21000 }); assert.equal(record.totalActiveMs + record.loadingMs, 9800 + 12000); assert.ok(record.inputMs.mouse <= 6000); }); test('eligible coverage completes a pass and a reread starts a fresh pass', () => { const version = { pages: [{ t: ['cover'] }, { t: [] }, { t: ['extra'] }, { t: [] }] }; const eligible = eligiblePages(version, { skipCovers: true, skipExtras: true }); assert.deepEqual(eligible, [1, 3]); const record = emptyRecord(); applyPulse(record, pulse('first-pass', 1, { activeMs: 1000, inputMs: { keyboard: 1000 }, seen: [0, 1, 2, 3] }), { version: 'source-a', pageCount: 4, eligible, now: 1000, }); assert.equal(record.completedPasses, 1); assert.equal(record.firstCompleteMs, 1000); assert.deepEqual(record.currentPass.seenRanges, [[1, 1], [3, 3]]); applyPulse(record, pulse('second-pass', 1), { version: 'source-a', pageCount: 4, eligible, now: 2000 }); const summary = publicSummary(record, 'source-a', eligible); assert.equal(summary.currentPass.complete, false); assert.equal(summary.currentPass.seenPages, 0); assert.equal(summary.totalActiveMs, 1000); }); test('switching source versions starts separate page coverage without losing lifetime time', () => { const record = emptyRecord(); applyPulse(record, pulse('version-test', 1, { activeMs: 800, seen: [0] }), { version: 'source-a', pageCount: 2, eligible: [0, 1], now: 1000, }); applyPulse(record, pulse('version-test', 2, { activeMs: 500, seen: [2] }), { version: 'source-b', pageCount: 3, eligible: [0, 1, 2], now: 2000, }); const summary = publicSummary(record, 'source-b', [0, 1, 2]); assert.equal(summary.totalActiveMs, 1300); assert.deepEqual(summary.currentPass.seenRanges, [[2, 2]]); }); test('chapter and manga insights distinguish untimed, partial, complete and reread records', () => { assert.equal(chapterInsight(null), null); const records = { first: { ...emptyRecord(), totalActiveMs: 60000, updatedAt: 100, currentPass: { activeMs: 60000 } }, second: { ...emptyRecord(), totalActiveMs: 540000, lastCompleteMs: 180000, firstCompleteMs: 210000, completedPasses: 2, updatedAt: 300, currentPass: { activeMs: 180000, completedAt: 300 } }, third: { ...emptyRecord(), totalActiveMs: 300000, lastCompleteMs: 300000, firstCompleteMs: 300000, completedPasses: 1, updatedAt: 200, currentPass: { activeMs: 300000, completedAt: 200 } }, }; assert.deepEqual(chapterInsight(records.first), { totalActiveMs: 60000, firstCompleteMs: null, lastCompleteMs: null, completedPasses: 0, currentPassMs: 60000, currentPassComplete: false, updatedAt: 100, }); assert.deepEqual(mangaInsight(records, ['first', 'second', 'third', 'missing']), { activeMs: 900000, timedChapters: 3, completedChapters: 2, completedPasses: 3, rereads: 1, typicalChapterMs: 240000, lastChapterId: 'second', updatedAt: 300, }); }); test('library insights select the most-read and most-recent manga', () => { const summary = libraryInsight([ { id: 'a', reading: { activeMs: 1000, timedChapters: 1, completedPasses: 1, rereads: 0, updatedAt: 100, lastChapterId: '1/1' } }, { id: 'b', reading: { activeMs: 3000, timedChapters: 2, completedPasses: 4, rereads: 2, updatedAt: 90, lastChapterId: '1/2' } }, { id: 'empty', reading: { activeMs: 0, timedChapters: 0, completedPasses: 0, rereads: 0, updatedAt: 0 } }, ]); assert.deepEqual(summary, { activeMs: 4000, timedChapters: 3, completedPasses: 5, rereads: 2, timedMangas: 2, mostTimeMangaId: 'b', lastMangaId: 'a', lastChapterId: '1/1', updatedAt: 100, }); });