import { describe, it, expect, vi, beforeEach } from 'vitest'; import { WorldBlockCache } from '../WorldBlockCache.js'; function makeDb() { const blocks = new Map(); return { getBlock: vi.fn((x, y, z) => { const key = `${x},${y},${z}`; return blocks.get(key) || null; }), getWorldBlocks: vi.fn((limit) => { const all = []; for (const [key, val] of blocks) { const [x, y, z] = key.split(',').map(Number); all.push({ x, y, z, block_name: val.name, metadata: val.metadata || 0, discovered_by: val.discoveredBy, discovered_at: val.timestamp }); if (all.length >= limit) break; } return all; }), getWorldBlockCount: vi.fn(() => blocks.size), getWorldBlocksInArea: vi.fn(() => []), // Helper for test setup _blocks: blocks, _addBlock(x, y, z, name) { blocks.set(`${x},${y},${z}`, { name, metadata: 0, discoveredBy: 1, timestamp: Date.now() }); } }; } describe('WorldBlockCache', () => { let db, cache; beforeEach(() => { db = makeDb(); cache = new WorldBlockCache(db, 5); // Small capacity for testing eviction }); it('should return undefined for missing blocks', () => { expect(cache.get('0,0,0')).toBeUndefined(); expect(db.getBlock).toHaveBeenCalledWith(0, 0, 0); }); it('should cache blocks from DB on first access', () => { db._addBlock(1, 2, 3, 'minecraft:stone'); const block = cache.get('1,2,3'); expect(block).toBeDefined(); expect(block.name).toBe('minecraft:stone'); // Second access should not hit DB cache.get('1,2,3'); expect(db.getBlock).toHaveBeenCalledTimes(1); }); it('should set and retrieve blocks', () => { cache.set('5,5,5', { name: 'minecraft:dirt', metadata: 0 }); const block = cache.get('5,5,5'); expect(block.name).toBe('minecraft:dirt'); // Should not hit DB since we just set it expect(db.getBlock).not.toHaveBeenCalled(); }); it('should delete blocks from cache', () => { cache.set('1,1,1', { name: 'minecraft:stone' }); expect(cache.delete('1,1,1')).toBe(true); // Now should fall through to DB const result = cache.get('1,1,1'); expect(db.getBlock).toHaveBeenCalledWith(1, 1, 1); }); it('should evict LRU entries when over capacity', () => { // Fill cache to capacity (5) for (let i = 0; i < 5; i++) { cache.set(`${i},0,0`, { name: `block_${i}` }); } // Access block_0 to make it recently used cache.get('0,0,0'); // Add one more → should evict the LRU entry (block_1, since block_0 was just accessed) cache.set('5,0,0', { name: 'block_5' }); // block_1 should have been evicted (we check internal cache size) expect(cache._cache.size).toBe(5); expect(cache._cache.has('1,0,0')).toBe(false); expect(cache._cache.has('0,0,0')).toBe(true); // recently used }); it('should report size from DB count', () => { db._addBlock(1, 1, 1, 'stone'); db._addBlock(2, 2, 2, 'dirt'); expect(cache.size).toBe(2); expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1); // Cached — no second call const _ = cache.size; expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1); }); it('should invalidate size cache on set/delete', () => { const _ = cache.size; expect(db.getWorldBlockCount).toHaveBeenCalledTimes(1); cache.set('1,1,1', { name: 'stone' }); const __ = cache.size; expect(db.getWorldBlockCount).toHaveBeenCalledTimes(2); }); it('should iterate all entries from DB', () => { db._addBlock(1, 1, 1, 'stone'); db._addBlock(2, 2, 2, 'dirt'); const entries = [...cache.entries()]; expect(entries).toHaveLength(2); expect(db.getWorldBlocks).toHaveBeenCalled(); }); it('should return blocks formatted for API', () => { db._addBlock(3, 3, 3, 'minecraft:diamond_ore'); const blocks = cache.getAllBlocksForAPI(100); expect(blocks).toHaveLength(1); expect(blocks[0]).toMatchObject({ x: 3, y: 3, z: 3, name: 'minecraft:diamond_ore' }); }); });