From 23a87099c5250d48a72813b608d26a3b3c39c634 Mon Sep 17 00:00:00 2001 From: maashkinoozhe Date: Sun, 25 Oct 2026 09:00:41 +0100 Subject: [PATCH] Discovery store keeps harvest, settings and nightly state (BL-0051) Co-Authored-By: Claude Opus 5.5 --- discovery/store.js | 21 +++++++++++++++++++-- test/discovery.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/discovery/store.js b/discovery/store.js index def28c2..44e0ebc 100644 --- a/discovery/store.js +++ b/discovery/store.js @@ -4,6 +4,8 @@ const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); +const META_KEYS = ['harvest', 'settings', 'nightly']; + class DiscoveryStore { constructor(dataDir) { this.file = path.join(path.resolve(dataDir), 'discovery.json'); } load() { @@ -11,9 +13,14 @@ class DiscoveryStore { const data = JSON.parse(fs.readFileSync(this.file, 'utf8')); if (data.schema !== 1 || !Number.isSafeInteger(data.revision) || !data.candidates || !data.feeds || !data.lastRun) throw new Error('Invalid discovery store schema'); + // Phase A files lack the metadata fields; default them. + if (data.harvest === undefined) data.harvest = null; + if (data.settings === undefined) data.settings = null; + if (!data.nightly) data.nightly = { lastDate: null }; return data; } catch (error) { - if (error.code === 'ENOENT') return { schema: 1, revision: 0, candidates: {}, feeds: {}, lastRun: { status: 'never' } }; + if (error.code === 'ENOENT') return { schema: 1, revision: 0, candidates: {}, feeds: {}, lastRun: { status: 'never' }, + harvest: null, settings: null, nightly: { lastDate: null } }; throw new Error(`Discovery store unavailable; refusing to overwrite it: ${error.message}`); } } @@ -21,7 +28,17 @@ class DiscoveryStore { const prior = this.load(); const data = { schema: 1, revision: prior.revision + 1, candidates: value.candidates, feeds: value.feeds, lastRun: { status: 'success', startedAt: value.startedAt, finishedAt: new Date().toISOString(), - candidateCount: Object.keys(value.candidates).length, examined: value.examined, clipped: value.clipped } }; + candidateCount: Object.keys(value.candidates).length, examined: value.examined, clipped: value.clipped }, + harvest: prior.harvest, settings: prior.settings, nightly: prior.nightly }; + return this.write(data); + } + // Metadata only: each given key replaces that field; revision stays. + patch(fields) { + const data = this.load(); + for (const key of META_KEYS) if (key in fields) data[key] = fields[key]; + return this.write(data); + } + write(data) { fs.mkdirSync(path.dirname(this.file), { recursive: true }); const temp = `${this.file}.${crypto.randomBytes(6).toString('hex')}.tmp`; try { diff --git a/test/discovery.test.js b/test/discovery.test.js index 078ddb5..c8a43fe 100644 --- a/test/discovery.test.js +++ b/test/discovery.test.js @@ -46,3 +46,42 @@ test('discovery store publishes atomically and refuses to overwrite corrupt data assert.throws(() => store.publish({ candidates: {}, feeds: {} }), /refusing to overwrite/); assert.equal(fs.readFileSync(store.file, 'utf8'), '{broken'); }); + +function tempStore(t) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-discovery-meta-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + return new DiscoveryStore(dir); +} +const emptyRun = () => ({ candidates: {}, feeds: {}, startedAt: new Date().toISOString(), examined: 0, clipped: 0 }); + +test('store loads a Phase A file without new fields', t => { + const store = tempStore(t); + fs.writeFileSync(store.file, JSON.stringify({ schema: 1, revision: 3, candidates: {}, feeds: {}, lastRun: { status: 'success' } })); + const data = store.load(); + assert.equal(data.revision, 3); + assert.equal(data.harvest, null); + assert.equal(data.settings, null); + assert.deepEqual(data.nightly, { lastDate: null }); +}); + +test('patch keeps revision and publish keeps harvest', t => { + const store = tempStore(t); + store.publish(emptyRun()); + store.patch({ harvest: { status: 'success' } }); + assert.equal(store.load().revision, 1); + store.patch({ settings: { a: 1 } }); + store.patch({ nightly: { lastDate: '2026-10-25' } }); + store.publish(emptyRun()); + const data = store.load(); + assert.equal(data.harvest.status, 'success'); + assert.deepEqual(data.settings, { a: 1 }); + assert.equal(data.nightly.lastDate, '2026-10-25'); + assert.equal(data.revision, 2); +}); + +test('patch refuses a corrupt store', t => { + const store = tempStore(t); + fs.writeFileSync(store.file, '{broken'); + assert.throws(() => store.patch({ harvest: {} }), /refusing to overwrite/); + assert.equal(fs.readFileSync(store.file, 'utf8'), '{broken'); +});