Discovery final review fixes: lean publish, robust scoring, cheap prune (BL-0051)
- Cache prune counts directory entries first and only reads files when over the cap; tests for cross-kind prune, schema-mismatch and corrupt entries. - Scoring tolerates malformed manga.json and cached records (non-list tags, object or null relations, non-object items); a tag without a rank no longer passes the top-tag filter; authors without an integer AniList id are dropped. - discovery.json publishes only candidates some feed shows; examined/clipped still describe the pool. Tests that asserted the published count equals the pool size now check the pool via candidatePool() and the published set against the feeds. - The post-harvest rescore failure test now breaks discovery.json right before the publish (via a scraper stand-in) instead of relying on tags: 5, and a following successful harvest clears the stale lastError. - Consent text names everything sent to AniList; paused label mentions admin harvests; the UI test asserts the XSS seed instead of skipping it. - Slot-mix test proves the fresh/outside reservations matter. - Docs: privacy note, fresh fallback reason, per-period cap, dry-run wording, published-candidate rule; plan/spec hygiene and spec status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
613db96355
commit
cd6a30a76b
18 files changed
+250
-65
No files matched your search
@@ -49,6 +49,48 @@ test('prune keeps the newest maxFiles', (t) => {
|
||||
assert.notEqual(cache.get('title', 5), null);
|
||||
});
|
||||
|
||||
test('prune removes the oldest entries across kinds', (t) => {
|
||||
const { clock, cache } = setup(t, { maxFiles: 3 });
|
||||
const kinds = ['title', 'recs', 'creator', 'fresh', 'tagTop'];
|
||||
kinds.forEach((kind, i) => { clock.t += 1000; cache.put(kind, i, { i }); });
|
||||
assert.equal(cache.stats().files, 3);
|
||||
assert.equal(cache.get('title', 0), null);
|
||||
assert.equal(cache.get('recs', 1), null);
|
||||
for (const [i, kind] of kinds.entries()) if (i >= 2) assert.notEqual(cache.get(kind, i), null, kind);
|
||||
});
|
||||
|
||||
test('a write under the cap does not read other entries', (t) => {
|
||||
const { dir, cache } = setup(t, { maxFiles: 10 });
|
||||
cache.put('recs', 7, {});
|
||||
const file = path.join(dir, 'recs', fs.readdirSync(path.join(dir, 'recs'))[0]);
|
||||
fs.writeFileSync(file, '{broken');
|
||||
cache.put('title', 1, {});
|
||||
assert.equal(fs.readFileSync(file, 'utf8'), '{broken');
|
||||
});
|
||||
|
||||
test('over the cap, corrupt and schema-mismatch entries go before valid ones', (t) => {
|
||||
const { dir, clock, cache } = setup(t, { maxFiles: 2 });
|
||||
cache.put('recs', 1, {});
|
||||
cache.put('recs', 2, {});
|
||||
const [first, second] = fs.readdirSync(path.join(dir, 'recs')).map(name => path.join(dir, 'recs', name));
|
||||
fs.writeFileSync(first, '{broken');
|
||||
fs.writeFileSync(second, JSON.stringify({ schema: 2, kind: 'recs', key: 2, fetchedAt: new Date(clock.t).toISOString(), data: {} }));
|
||||
clock.t += 1000;
|
||||
cache.put('title', 3, {});
|
||||
assert.equal(fs.existsSync(first), false);
|
||||
assert.equal(fs.existsSync(second), false);
|
||||
assert.notEqual(cache.get('title', 3), null);
|
||||
});
|
||||
|
||||
test('schema-mismatch entry is deleted like a corrupt one', (t) => {
|
||||
const { dir, clock, cache } = setup(t);
|
||||
cache.put('recs', 7, {});
|
||||
const file = path.join(dir, 'recs', fs.readdirSync(path.join(dir, 'recs'))[0]);
|
||||
fs.writeFileSync(file, JSON.stringify({ schema: 2, kind: 'recs', key: 7, fetchedAt: new Date(clock.t).toISOString(), data: {} }));
|
||||
assert.equal(cache.get('recs', 7), null);
|
||||
assert.equal(fs.existsSync(file), false);
|
||||
});
|
||||
|
||||
test('corrupt entry is deleted', (t) => {
|
||||
const { dir, cache } = setup(t);
|
||||
cache.put('recs', 7, {});
|
||||
|
||||
Reference in new issue
Block a user