Files
MangaCircus/test/chapter-dates.test.js
T
MaashkinoozheandClaude Opus 5.5 e0dacd81c6 Chapter dates: read date setting and tiered release dates (BL-0047)
- Chapter rows show when you first finished a chapter (setting: date + hour,
  date, off) from a new durable progress doneAt; older reads fall back to the
  reading clock's earliest remembered completion.
- chapter.json keeps added (arrival in this library, now kept on replace) plus
  x-dates.source (source upload, bulk imports flagged), x-dates.release
  (official, for a later metadata job) and x-dates.ecosystem (first entry into
  any Manga Circus library; carried over peering, earliest wins, clamped).
- Rows show one release date by tier: official > source upload > ecosystem >
  library, with all known dates in the tooltip (setting to hide).
- Fixes: WeebCentral dates came from the chapter page, which has none (every
  chapter got the download time); they now come from the full chapter list.
  MangaDex no longer writes a group's publishAt into added. Peering sends dates.
- tools/backfill-chapter-dates.js migrates an existing library (dry run default).
- Coverage dry run on the server: docs/chapter-dates-coverage-2026-10-24.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:01:33 +02:00

82 lines
5.0 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { batchFlags, publishedDates, chapterDates, peerDates, fromPeer } = require('../chapter-dates');
const { earliestCompletion } = require('../reading-analytics');
test('bulk imports are flagged, weekly uploads are not', () => {
const flags = batchFlags([
'2024-09-07T10:00:00Z', '2024-09-07T10:00:03Z', '2024-09-07T10:04:00Z', // one import
'2026-10-11T15:05:58Z', '2026-10-18T15:06:05Z', // weekly releases
'2026-10-20T09:00:00Z', '2026-10-20T09:01:00Z', // a double release stays a release
undefined,
]);
assert.deepEqual(flags, [true, true, true, false, false, false, false, null]);
});
test('publishing keeps the first arrival and the earliest ecosystem date', () => {
const now = Date.parse('2026-10-24T12:00:00Z');
const fresh = publishedDates({}, { source: { at: '2026-10-18T15:06:05Z', name: 'WeebCentral', batch: false } }, now);
assert.equal(fresh.added, '2026-10-24T12:00:00.000Z');
assert.deepEqual(fresh['x-dates'].ecosystem, { at: '2026-10-24T12:00:00.000Z', via: 'this server' });
assert.equal(fresh['x-dates'].source.name, 'WeebCentral');
// Replacing a version later: "added" and the ecosystem date survive.
const replaced = publishedDates(fresh, {}, now + 864e5);
assert.equal(replaced.added, fresh.added);
assert.deepEqual(replaced['x-dates'].ecosystem, fresh['x-dates'].ecosystem);
assert.equal(replaced['x-dates'].source.at, '2026-10-18T15:06:05.000Z');
// A peer that had it earlier lowers the date; a later claim never raises it.
const earlier = publishedDates(fresh, { ecosystem: { at: '2026-10-19T08:00:00Z', via: 'peer bob' } }, now);
assert.deepEqual(earlier['x-dates'].ecosystem, { at: '2026-10-19T08:00:00.000Z', via: 'peer bob' });
const later = publishedDates(earlier, { ecosystem: { at: '2026-10-23T00:00:00Z', via: 'peer eve' } }, now);
assert.equal(later['x-dates'].ecosystem.via, 'peer bob');
});
test('peer ecosystem claims cannot predate the source upload or lie in the future', () => {
const now = Date.parse('2026-10-24T12:00:00Z');
const source = { at: '2026-10-18T15:06:05Z', name: 'WeebCentral', batch: false };
const early = publishedDates({}, { source, ecosystem: { at: '2001-01-01T00:00:00Z', via: 'peer x' } }, now);
assert.equal(early['x-dates'].ecosystem.at, '2026-10-18T15:06:05.000Z');
const future = publishedDates({}, { ecosystem: { at: '2099-01-01T00:00:00Z', via: 'peer x' } }, now);
assert.equal(future['x-dates'].ecosystem.at, '2026-10-24T12:00:00.000Z');
});
test('release date tiers: official, source upload, ecosystem, library', () => {
const added = Date.parse('2026-10-17T15:04:03Z');
const version = dates => ({ added, meta: { 'x-dates': dates } });
const ecosystem = { at: '2026-10-10T00:00:00Z', via: 'peer bob' };
assert.equal(chapterDates([version({})]).tier, 'library');
assert.equal(chapterDates([version({ ecosystem })]).release.at, '2026-10-10T00:00:00.000Z');
const batch = chapterDates([version({ ecosystem, source: { at: '2024-09-07T10:00:00Z', name: 'WeebCentral', batch: true } })]);
assert.equal(batch.tier, 'ecosystem', 'a bulk upload is no release date');
assert.equal(batch.known.batch.at, '2024-09-07T10:00:00.000Z');
const source = chapterDates([version({ ecosystem }), version({ source: { at: '2026-10-04T15:06:05Z', name: 'MangaDex', batch: false } })]);
assert.equal(source.tier, 'source');
assert.equal(source.known.ecosystem.at, '2026-10-10T00:00:00.000Z', 'earliest across versions');
const official = chapterDates([version({ release: { at: '2023-09-17', precision: 'day', source: 'MangaUpdates · Viz' }, ecosystem })]);
assert.deepEqual(official.release, { at: '2023-09-17', precision: 'day', name: 'MangaUpdates · Viz' });
assert.equal(official.tier, 'official');
});
test('peers exchange cleaned dates and the receiver records the peer', () => {
const meta = { added: '2026-10-17T15:04:03Z', secret: 'x',
'x-dates': { source: { at: '2026-10-18T15:06:05Z', name: 'WeebCentral', batch: false, extra: 'drop' } } };
const sent = peerDates(meta, 0);
assert.deepEqual(sent, { ecosystem: '2026-10-17T15:04:03.000Z', source: { at: '2026-10-18T15:06:05.000Z', name: 'WeebCentral', batch: false } });
const received = fromPeer(sent, 'bob');
assert.deepEqual(received.ecosystem, { at: '2026-10-17T15:04:03.000Z', via: 'peer bob' });
assert.deepEqual(fromPeer('nonsense', 'bob'), {});
assert.deepEqual(fromPeer({ ecosystem: 'not a date' }, 'bob'), {});
});
test('legacy reads get the earliest completion the reading clock remembers', () => {
assert.equal(earliestCompletion(null), null);
const record = { completedPasses: 3, currentPass: { completedAt: 5000 },
recentSessions: [{ pass: { completedAt: 3000 } }, { pass: { completedAt: null } }, { pass: { completedAt: 5000 } }] };
assert.deepEqual(earliestCompletion(record), { at: 3000, approx: true });
assert.deepEqual(earliestCompletion({ ...record, completedPasses: 2 }), { at: 3000, approx: false });
});