diff --git a/server/__tests__/WorldBlockCache.test.js b/server/__tests__/WorldBlockCache.test.js new file mode 100644 index 0000000..edc054f --- /dev/null +++ b/server/__tests__/WorldBlockCache.test.js @@ -0,0 +1,127 @@ +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' }); + }); +});