import test from 'node:test'; import assert from 'node:assert/strict'; import { createVerifierSaltCache } from '../src/core/verifier-salt-cache.mjs'; test('verifier salt cache cached fresh read returns without calling the verifier loader', async () => { let nowMs = 1_000; let calls = 0; const cache = createVerifierSaltCache({ loadSalt: async () => { calls += 1; return '252812b3'; }, maxAgeMs: 500, now: () => nowMs, }); const refreshed = await cache.refreshNow(); nowMs += 100; const cached = cache.getCachedFreshSalt(); assert.equal(calls, 1); assert.equal(refreshed.source, 'refresh'); assert.equal(cached.source, 'cache'); assert.equal(cached.available, true); assert.equal(cached.currentSaltHex, '252812b3'); assert.equal(cached.ageMs, 100); assert.equal(cache.getState().salt_fresh, true); }); test('verifier salt cache missing or stale cached read returns unavailable without loader call', async () => { let nowMs = 1_000; let calls = 0; const cache = createVerifierSaltCache({ loadSalt: async () => { calls += 1; return '252812b3'; }, maxAgeMs: 500, now: () => nowMs, }); const missing = cache.getCachedFreshSalt(); assert.equal(missing.available, false); assert.equal(missing.reason, 'empty_cache'); assert.equal(calls, 0); await cache.refreshNow(); nowMs += 501; const stale = cache.getCachedFreshSalt(); assert.equal(stale.available, false); assert.equal(stale.reason, 'stale_cache'); assert.equal(stale.ageMs, 501); assert.equal(calls, 1); assert.equal(cache.getState().salt_fresh, false); }); test('verifier salt cache cached read does not wait for slow background current_salt refresh', async () => { let calls = 0; let resolveSalt; const slowSalt = new Promise((resolve) => { resolveSalt = resolve; }); const cache = createVerifierSaltCache({ loadSalt: async () => { calls += 1; return slowSalt; }, setIntervalFn: () => ({ unref() {} }), clearIntervalFn: () => {}, }); cache.start(); await Promise.resolve(); const cached = cache.getCachedFreshSalt(); assert.equal(cached.available, false); assert.equal(cached.reason, 'empty_cache'); assert.equal(cached.state.refresh_in_flight, true); assert.equal(calls, 1); resolveSalt('252812b3'); await cache.refreshNow(); assert.equal(cache.getCachedFreshSalt().available, true); }); test('verifier salt cache background refresh records success duration and state', async () => { let nowMs = 1_000; const cache = createVerifierSaltCache({ loadSalt: async () => { nowMs += 37; return '252812b3'; }, maxAgeMs: 500, now: () => nowMs, }); const refreshed = await cache.refreshNow({ reason: 'unit_test' }); const state = cache.getState(); assert.equal(refreshed.currentSaltHex, '252812b3'); assert.equal(state.salt_fresh, true); assert.equal(state.last_refresh_started_at, '1970-01-01T00:00:01.000Z'); assert.equal(state.last_refresh_completed_at, '1970-01-01T00:00:01.037Z'); assert.equal(state.last_refresh_duration_ms, 37); assert.equal(state.last_refresh_reason, 'unit_test'); assert.equal(state.refresh_in_flight, false); assert.equal(state.last_refresh_error, null); }); test('verifier salt cache refresh failure preserves still-fresh cached salt', async () => { let nowMs = 1_000; let fail = false; const cache = createVerifierSaltCache({ loadSalt: async () => { if (fail) throw new Error('rpc unavailable'); return '252812b3'; }, maxAgeMs: 500, now: () => nowMs, }); await cache.refreshNow(); nowMs += 100; fail = true; await assert.rejects( () => cache.refreshNow({ reason: 'prefetch' }), /rpc unavailable/, ); const cached = cache.getCachedFreshSalt(); assert.equal(cached.available, true); assert.equal(cached.currentSaltHex, '252812b3'); assert.equal(cached.ageMs, 100); assert.equal(cache.getState().fresh, true); assert.equal(cache.getState().last_refresh_error.message, 'rpc unavailable'); }); test('verifier salt cache rejects malformed salts before signing can use them', async () => { const cache = createVerifierSaltCache({ loadSalt: async () => 'not-a-salt', }); await assert.rejects( () => cache.refreshNow(), /current_salt must be 4 bytes in hex/, ); assert.equal(cache.getState().has_cached_salt, false); assert.equal(cache.getCachedFreshSalt().available, false); });