Discover page: refresh, harvest admin and seed reasons; docs (BL-0051)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaashkinoozheandClaude Opus 5.5 committed 2026-10-25 09:28:38 +01:00
1 parent 0f87fbe948
commit 613db96355
10 files changed
+265 -58

No files matched your search

+26 -1
View File
@@ -3,6 +3,7 @@
const test = require('node:test'), assert = require('node:assert/strict');
const fs = require('fs'), os = require('os'), path = require('path'), net = require('net'), http = require('http'), crypto = require('crypto');
const { spawn } = require('child_process');
const { Canvas } = require('../tools/png');
const FIXTURES = path.join(__dirname, 'fixtures', 'anilist');
const fixture = name => JSON.parse(fs.readFileSync(path.join(FIXTURES, `${name}.json`), 'utf8'));
@@ -49,11 +50,17 @@ const sha = s => crypto.createHash('sha256').update(s).digest('hex');
const USERS = { boss: { admin: true, discovery: false }, alice: { discovery: true }, bob: { discovery: true }, carol: { discovery: false } };
// A disposable reader with ready-made accounts and sessions; nightly harvests are off unless the store says otherwise.
async function startReader({ anilist, discovery = {} } = {}) {
async function startReader({ anilist, discovery = {}, mangaMeta = null } = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-discovery-server-'));
const library = path.join(root, 'library'), data = path.join(root, 'data');
fs.mkdirSync(path.join(library, 'Example Manga'), { recursive: true });
fs.writeFileSync(path.join(library, 'Example Manga', 'notes.txt'), 'not a chapter');
if (mangaMeta) {
const chapter = path.join(library, mangaMeta.title, 'v01', 'c0001');
fs.mkdirSync(chapter, { recursive: true });
fs.writeFileSync(path.join(chapter, '0001.png'), new Canvas(20, 30, [60, 90, 140]).png());
fs.writeFileSync(path.join(library, mangaMeta.title, 'manga.json'), JSON.stringify(mangaMeta));
}
fs.mkdirSync(path.join(data, 'scraper'), { recursive: true });
fs.writeFileSync(path.join(data, 'scraper', 'queue.json'), JSON.stringify({ queue: [] }));
const users = {}, sessions = {}, cookies = {};
@@ -209,6 +216,24 @@ test('admin harvest is admin-only and single-flight', { timeout: 60000 }, async
} finally { await reader.stop(); await anilist.close(); }
});
test('a failed rescore after a harvest reaches the admin as lastError', { timeout: 60000 }, async () => {
const anilist = await startAniList();
// A manga.json whose tags is not a list makes scoring throw; it stands in for any scoring fault.
const reader = await startReader({ anilist, mangaMeta: { title: 'Broken Tags', tags: 5 } });
try {
await until(async () => (await reader.get('boss', '/api/library')).body?.mangas?.length === 1, 'the library scan');
assert.equal((await reader.post('boss', '/api/admin/discovery/harvest')).status, 200);
const done = await harvestDone(reader);
assert.equal(done.harvest.kind, 'admin');
const admin = await until(async () => {
const { body } = await reader.get('boss', '/api/admin/discovery');
return body.lastError ? body : null;
}, 'the rescore error');
assert.match(admin.lastError, /^Re-scoring after harvest [0-9a-f]{16} failed: /);
assert.equal(admin.currentRun, null);
} finally { await reader.stop(); await anilist.close(); }
});
test('settings validation', { timeout: 30000 }, async () => {
const reader = await startReader();
try {