Files

208 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
});
});