Discovery store keeps harvest, settings and nightly state (BL-0051)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
62ec69bc0d
commit
23a87099c5
2 files changed
+58
-2
No files matched your search
+19
-2
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user