'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 { DiscoveryCache } = require('../discovery/cache'); const DAY = 86400000; function setup(t, options = {}) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'circus-dcache-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const clock = { t: Date.UTC(2026, 0, 1) }; const cache = new DiscoveryCache(dir, { ...options, now: () => clock.t }); return { dir, clock, cache }; } test('cache round-trips an entry', (t) => { const { clock, cache } = setup(t); cache.put('title', 42, { a: 1 }); const entry = cache.get('title', 42); assert.deepEqual(entry.data, { a: 1 }); assert.equal(entry.fetchedAt, new Date(clock.t).toISOString()); assert.equal(entry.schema, 1); assert.equal(entry.kind, 'title'); }); test('freshness follows TTL per kind', (t) => { const { clock, cache } = setup(t); cache.put('fresh', 'global', []); cache.put('title', 1, {}); clock.t += 8 * DAY; assert.equal(cache.isFresh('fresh', 'global'), false); assert.equal(cache.isFresh('title', 1), true); clock.t += 23 * DAY; assert.equal(cache.isFresh('title', 1), false); assert.notEqual(cache.get('title', 1), null); assert.equal(cache.list('title').length, 1); }); test('prune keeps the newest maxFiles', (t) => { const { clock, cache } = setup(t, { maxFiles: 3 }); for (let i = 1; i <= 5; i++) { clock.t += 1000; cache.put('title', i, { i }); } assert.equal(cache.stats().files, 3); assert.equal(cache.get('title', 1), null); assert.equal(cache.get('title', 2), null); 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, {}); const file = path.join(dir, 'recs', fs.readdirSync(path.join(dir, 'recs'))[0]); fs.writeFileSync(file, '{broken'); assert.equal(cache.get('recs', 7), null); assert.equal(fs.existsSync(file), false); }); test('odd keys are file-safe', (t) => { const { dir, cache } = setup(t); cache.put('tagTop', 'genre:Slice of Life/../x', [1]); const files = fs.readdirSync(path.join(dir, 'tagTop')); assert.equal(files.length, 1); assert.deepEqual(fs.readdirSync(dir), ['tagTop']); assert.deepEqual(cache.get('tagTop', 'genre:Slice of Life/../x').data, [1]); }); test('unknown kind throws', (t) => { const { cache } = setup(t); assert.throws(() => cache.put('nope', 1, {}), TypeError); assert.throws(() => cache.get('nope', 1), TypeError); });