208 lines
7.4 KiB
JavaScript
208 lines
7.4 KiB
JavaScript
/**
|
||
* Tests for the Inventory Manager database layer (db.js).
|
||
*
|
||
* Sets DB_PATH to a temp file BEFORE importing the module so all
|
||
* operations hit a throwaway SQLite database. The underlying
|
||
* better-sqlite3 `Database` instance is also imported so we can
|
||
* DELETE rows between tests without reopening the file.
|
||
*/
|
||
|
||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
||
import fs from 'fs';
|
||
import os from 'os';
|
||
import path from 'path';
|
||
|
||
// DB_PATH is set via vitest.config.js env before this module loads.
|
||
const TMP_DB = process.env.DB_PATH;
|
||
import db from '../db.js';
|
||
import {
|
||
saveItems, loadItems,
|
||
saveState, loadState,
|
||
saveFurnaces, loadFurnaces,
|
||
saveAlerts, loadAlerts,
|
||
recordItemHistory, getHistory, getHistorySummary,
|
||
saveFullState, flushPendingSave, loadFullState,
|
||
closeDb,
|
||
} from '../db.js';
|
||
|
||
// ── helpers ────────────────────────────────────────────────────────────
|
||
|
||
/** Wipe all rows from every table so each test starts fresh */
|
||
function clearAllTables() {
|
||
db.exec(`
|
||
DELETE FROM items;
|
||
DELETE FROM furnaces;
|
||
DELETE FROM alerts;
|
||
DELETE FROM state;
|
||
DELETE FROM item_history;
|
||
`);
|
||
}
|
||
|
||
// ── lifecycle ──────────────────────────────────────────────────────────
|
||
|
||
beforeEach(() => {
|
||
clearAllTables();
|
||
});
|
||
|
||
afterAll(() => {
|
||
try { closeDb(); } catch { /* already closed */ }
|
||
try { fs.unlinkSync(TMP_DB); } catch { /* ignore */ }
|
||
try { fs.unlinkSync(TMP_DB + '-wal'); } catch { /* ignore */ }
|
||
try { fs.unlinkSync(TMP_DB + '-shm'); } catch { /* ignore */ }
|
||
});
|
||
|
||
// ── tests ──────────────────────────────────────────────────────────────
|
||
|
||
describe('db – item persistence', () => {
|
||
it('saveItems + loadItems round-trips item data', () => {
|
||
const items = [
|
||
{ name: 'minecraft:diamond', count: 64, displayName: 'Diamond' },
|
||
{ name: 'minecraft:iron_ingot', count: 128, displayName: 'Iron Ingot' },
|
||
];
|
||
saveItems(items);
|
||
|
||
const loaded = loadItems();
|
||
expect(loaded).toHaveLength(2);
|
||
|
||
// loadItems returns ORDER BY count DESC
|
||
expect(loaded[0].name).toBe('minecraft:iron_ingot');
|
||
expect(loaded[0].count).toBe(128);
|
||
expect(loaded[1].name).toBe('minecraft:diamond');
|
||
expect(loaded[1].count).toBe(64);
|
||
});
|
||
|
||
it('saveItems upserts — updates existing rows instead of duplicating', () => {
|
||
saveItems([{ name: 'minecraft:diamond', count: 10, displayName: 'Diamond' }]);
|
||
saveItems([{ name: 'minecraft:diamond', count: 99, displayName: 'Diamond' }]);
|
||
|
||
const loaded = loadItems();
|
||
expect(loaded).toHaveLength(1);
|
||
expect(loaded[0].count).toBe(99);
|
||
});
|
||
});
|
||
|
||
describe('db – state key-value store', () => {
|
||
it('saveState + loadState round-trips JSON', () => {
|
||
saveState('smeltingPaused', true);
|
||
expect(loadState('smeltingPaused')).toBe(true);
|
||
|
||
saveState('disabledRecipes', { 'minecraft:iron_ingot': true });
|
||
expect(loadState('disabledRecipes')).toEqual({ 'minecraft:iron_ingot': true });
|
||
});
|
||
|
||
it('loadState returns defaultValue when key missing', () => {
|
||
expect(loadState('nonexistent', 42)).toBe(42);
|
||
expect(loadState('nonexistent')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('db – furnace persistence', () => {
|
||
it('saveFurnaces + loadFurnaces round-trips furnace status', () => {
|
||
const furnaces = {
|
||
'minecraft:furnace_0': {
|
||
active: true,
|
||
type: 'minecraft:furnace',
|
||
input: { name: 'minecraft:raw_iron', count: 8 },
|
||
fuel: { name: 'minecraft:coal', count: 3 },
|
||
output: null,
|
||
},
|
||
};
|
||
|
||
saveFurnaces(furnaces);
|
||
const loaded = loadFurnaces();
|
||
|
||
expect(loaded['minecraft:furnace_0']).toBeDefined();
|
||
expect(loaded['minecraft:furnace_0'].active).toBe(true);
|
||
expect(loaded['minecraft:furnace_0'].input.name).toBe('minecraft:raw_iron');
|
||
});
|
||
});
|
||
|
||
describe('db – alert persistence', () => {
|
||
it('saveAlerts + loadAlerts round-trips triggered alerts', () => {
|
||
saveAlerts([
|
||
{ item: 'minecraft:diamond', triggered: true, current: 5, threshold: 10 },
|
||
{ item: 'minecraft:coal', triggered: false, current: 200, threshold: 50 },
|
||
]);
|
||
|
||
const loaded = loadAlerts();
|
||
// loadAlerts only returns triggered=1
|
||
expect(loaded).toHaveLength(1);
|
||
expect(loaded[0].item).toBe('minecraft:diamond');
|
||
expect(loaded[0].triggered).toBe(true);
|
||
expect(loaded[0].current).toBe(5);
|
||
expect(loaded[0].threshold).toBe(10);
|
||
});
|
||
});
|
||
|
||
describe('db – item history', () => {
|
||
it('getHistory returns recorded snapshots ordered newest-first', () => {
|
||
// recordItemHistory is throttled (5-min internal cooldown).
|
||
// First call after module load will go through.
|
||
recordItemHistory([
|
||
{ name: 'minecraft:diamond', count: 64 },
|
||
{ name: 'minecraft:iron_ingot', count: 128 },
|
||
]);
|
||
|
||
const history = getHistory('minecraft:diamond', 10);
|
||
expect(history.length).toBeGreaterThanOrEqual(1);
|
||
expect(history[0].count).toBe(64);
|
||
});
|
||
|
||
it('getHistorySummary groups by timestamp', () => {
|
||
// May be throttled if previous test already recorded in this module load.
|
||
// Insert directly via the underlying db for determinism.
|
||
const now = Date.now();
|
||
db.prepare('INSERT INTO item_history (name, count, recorded_at) VALUES (?, ?, ?)').run('a', 10, now);
|
||
db.prepare('INSERT INTO item_history (name, count, recorded_at) VALUES (?, ?, ?)').run('b', 20, now);
|
||
|
||
const summary = getHistorySummary();
|
||
expect(summary.length).toBeGreaterThanOrEqual(1);
|
||
expect(summary[0].total).toBe(30); // 10 + 20
|
||
});
|
||
});
|
||
|
||
describe('db – full state round-trip', () => {
|
||
it('saveFullState + flushPendingSave + loadFullState restores everything', () => {
|
||
const state = {
|
||
inventoryState: {
|
||
itemList: [
|
||
{ name: 'minecraft:diamond', count: 64, displayName: 'Diamond' },
|
||
],
|
||
grandTotal: 64,
|
||
chestCount: 2,
|
||
totalSlots: 54,
|
||
usedSlots: 1,
|
||
freeSlots: 53,
|
||
usedRatio: 0.02,
|
||
dropperOk: true,
|
||
barrelOk: false,
|
||
furnaceCount: 1,
|
||
furnaceStatus: {
|
||
'furnace_0': { active: false, type: 'minecraft:furnace', input: null, fuel: null, output: null },
|
||
},
|
||
droppers: ['dropper_0'],
|
||
},
|
||
activityState: { lastScan: 12345 },
|
||
alertsState: [{ item: 'minecraft:coal', triggered: true, current: 3, threshold: 10 }],
|
||
smeltingPaused: false,
|
||
disabledRecipes: { 'minecraft:charcoal': true },
|
||
smeltableRecipes: { 'minecraft:raw_iron': 'minecraft:iron_ingot' },
|
||
craftableRecipes: [{ output: 'minecraft:chest', count: 1, slots: { 1: 'minecraft:planks' } }],
|
||
craftTurtleOk: true,
|
||
};
|
||
|
||
saveFullState(state);
|
||
flushPendingSave(); // force the debounced write
|
||
|
||
const restored = loadFullState();
|
||
expect(restored.inventoryState.itemList).toHaveLength(1);
|
||
expect(restored.inventoryState.itemList[0].name).toBe('minecraft:diamond');
|
||
expect(restored.inventoryState.grandTotal).toBe(64);
|
||
expect(restored.smeltingPaused).toBe(false);
|
||
expect(restored.disabledRecipes).toEqual({ 'minecraft:charcoal': true });
|
||
expect(restored.craftTurtleOk).toBe(true);
|
||
expect(restored.alertsState).toHaveLength(1);
|
||
expect(restored.alertsState[0].item).toBe('minecraft:coal');
|
||
});
|
||
});
|