'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { generate } = require('../discovery/engine'); const { DiscoveryStore } = require('../discovery/store'); test('discovery is bounded, diversified, opt-in, evidence-linked and user-specific', () => { const { loadCatalog } = require('../greatest'); const greatest = loadCatalog(); const users = { alice: { name: 'alice', settings: { discoveryEnabled: true } }, bob: { name: 'bob', settings: { discoveryEnabled: true } }, off: { name: 'off', settings: {} } }; const db = { users, greatestRatings: { alice: { [greatest.entries[0].id]: 'flame' }, bob: { [greatest.entries[20].id]: 'flame' } }, discoveryFeedback: { alice: {} }, progress: {} }; const result = generate({ db, library: [], greatest, cacheDir: path.join(os.tmpdir(), 'nonexistent-discovery-cache') }); assert.equal(Object.keys(result.candidates).length, 50); assert.ok(result.feeds.alice.items.length > 0 && result.feeds.alice.items.length <= 20); assert.ok(result.feeds.bob.items.length > 0); assert.equal(result.feeds.off.disabled, true); assert.ok(new Set(Object.values(result.candidates).map(c => c.period)).size >= 10, 'An early catalog page must not crowd out later eras'); assert.ok(result.feeds.alice.items.every(item => item.evidenceIds.every(id => item.evidence.some(e => e.id === id)))); assert.ok(result.feeds.alice.items.some(item => item.evidence.some(e => e.kind === 'personal'))); assert.ok(!result.feeds.bob.items.some(item => item.evidence.some(e => e.label?.includes(greatest.entries[0].title) && e.kind === 'personal'))); assert.notDeepEqual(result.feeds.alice.items.map(x => x.candidateId), result.feeds.bob.items.map(x => x.candidateId)); const first = result.feeds.alice.items[0].candidateId; db.discoveryFeedback.alice[first] = 'dismissed'; const rerun = generate({ db, library: [], greatest, cacheDir: path.join(os.tmpdir(), 'nonexistent-discovery-cache') }); assert.ok(!rerun.feeds.alice.items.some(item => item.candidateId === first)); assert.equal(rerun.feeds.bob.items.length, result.feeds.bob.items.length); }); test('discovery store publishes atomically and refuses to overwrite corrupt data', t => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-discovery-store-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const store = new DiscoveryStore(dir); assert.equal(store.load().revision, 0); const value = store.publish({ candidates: {}, feeds: {}, startedAt: new Date().toISOString(), examined: 0, clipped: 0 }); assert.equal(value.revision, 1); store.publish({ candidates: {}, feeds: {}, startedAt: new Date().toISOString(), examined: 0, clipped: 0 }); assert.equal(store.load().revision, 2); assert.ok(fs.existsSync(store.file + '.prev')); fs.writeFileSync(store.file, '{broken'); assert.throws(() => store.publish({ candidates: {}, feeds: {} }), /refusing to overwrite/); assert.equal(fs.readFileSync(store.file, 'utf8'), '{broken'); });