import { describe, it, expect, beforeEach } from 'vitest' import { _resetForTest, getInstances, getInstance, getDistinctStacks, createInstance, updateInstance, deleteInstance, importInstances, getInstanceHistory, } from '../server/db.js' beforeEach(() => _resetForTest()); // ── getInstances ────────────────────────────────────────────────────────────── describe('getInstances', () => { it('returns empty array when table is empty', () => { expect(getInstances()).toEqual([]); }); it('returns all instances sorted by name', () => { createInstance({ name: 'zebra', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'alpha', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); const result = getInstances(); expect(result[0].name).toBe('alpha'); expect(result[1].name).toBe('zebra'); }); it('filters by state', () => { createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'b', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'c', state: 'testing', stack: 'development', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getInstances({ state: 'deployed' })).toHaveLength(1); expect(getInstances({ state: 'degraded' })).toHaveLength(1); expect(getInstances({ state: 'testing' })).toHaveLength(1); }); it('filters by stack', () => { createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getInstances({ stack: 'production' })).toHaveLength(1); expect(getInstances({ stack: 'development' })).toHaveLength(1); }); it('searches by name', () => { createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'gitea', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getInstances({ search: 'ple' })).toHaveLength(1); expect(getInstances({ search: 'ple' })[0].name).toBe('plex'); }); it('searches by vmid', () => { createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 137, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'b', state: 'deployed', stack: 'production', vmid: 200, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getInstances({ search: '137' })).toHaveLength(1); }); it('combines filters', () => { createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'plex2', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getInstances({ search: 'plex', state: 'deployed' })).toHaveLength(1); }); }); // ── getInstance ─────────────────────────────────────────────────────────────── describe('getInstance', () => { it('returns the instance with the given vmid', () => { createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 117, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); const inst = getInstance(117); expect(inst).not.toBeNull(); expect(inst.name).toBe('plex'); expect(inst.vmid).toBe(117); }); it('returns null for unknown vmid', () => { expect(getInstance(999)).toBeNull(); }); }); // ── getDistinctStacks ───────────────────────────────────────────────────────── describe('getDistinctStacks', () => { it('returns empty array when table is empty', () => { expect(getDistinctStacks()).toEqual([]); }); it('returns unique stacks sorted alphabetically', () => { createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); createInstance({ name: 'c', state: 'deployed', stack: 'production', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }); expect(getDistinctStacks()).toEqual(['development', 'production']); }); }); // ── createInstance ──────────────────────────────────────────────────────────── describe('createInstance', () => { const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }; it('inserts a new instance and sets timestamps', () => { createInstance({ ...base, name: 'traefik', vmid: 100 }); const inst = getInstance(100); expect(inst.name).toBe('traefik'); expect(inst.created_at).not.toBeNull(); expect(inst.updated_at).not.toBeNull(); }); it('stores service flags correctly', () => { createInstance({ ...base, name: 'plex', vmid: 1, atlas: 1, tailscale: 1, hardware_acceleration: 1 }); const inst = getInstance(1); expect(inst.atlas).toBe(1); expect(inst.argus).toBe(0); expect(inst.tailscale).toBe(1); expect(inst.hardware_acceleration).toBe(1); }); it('rejects duplicate vmid', () => { createInstance({ ...base, name: 'a', vmid: 100 }); expect(() => createInstance({ ...base, name: 'b', vmid: 100 })).toThrow(); }); it('rejects invalid state', () => { expect(() => createInstance({ ...base, name: 'a', vmid: 1, state: 'invalid' })).toThrow(); }); it('rejects invalid stack', () => { expect(() => createInstance({ ...base, name: 'a', vmid: 1, stack: 'invalid' })).toThrow(); }); }); // ── updateInstance ──────────────────────────────────────────────────────────── describe('updateInstance', () => { const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }; it('updates fields and refreshes updated_at', () => { createInstance({ ...base, name: 'old', vmid: 100 }); updateInstance(100, { ...base, name: 'new', vmid: 100, state: 'degraded' }); const inst = getInstance(100); expect(inst.name).toBe('new'); expect(inst.state).toBe('degraded'); }); it('can change vmid', () => { createInstance({ ...base, name: 'a', vmid: 100 }); updateInstance(100, { ...base, name: 'a', vmid: 200 }); expect(getInstance(100)).toBeNull(); expect(getInstance(200)).not.toBeNull(); }); }); // ── deleteInstance ──────────────────────────────────────────────────────────── describe('deleteInstance', () => { const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }; it('removes the instance', () => { createInstance({ ...base, name: 'a', vmid: 1 }); deleteInstance(1); expect(getInstance(1)).toBeNull(); }); it('only removes the targeted instance', () => { createInstance({ ...base, name: 'a', vmid: 1 }); createInstance({ ...base, name: 'b', vmid: 2 }); deleteInstance(1); expect(getInstance(1)).toBeNull(); expect(getInstance(2)).not.toBeNull(); }); }); // ── importInstances ─────────────────────────────────────────────────────────── describe('importInstances', () => { const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }; it('replaces all existing instances with the imported set', () => { createInstance({ ...base, name: 'old', vmid: 1 }); importInstances([{ ...base, name: 'new', vmid: 2 }]); expect(getInstance(1)).toBeNull(); expect(getInstance(2)).not.toBeNull(); }); it('clears all instances when passed an empty array', () => { createInstance({ ...base, name: 'a', vmid: 1 }); importInstances([]); expect(getInstances()).toEqual([]); }); }); // ── instance history ───────────────────────────────────────────────────────── describe('instance history', () => { const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 }; it('logs a created event when an instance is created', () => { createInstance({ ...base, name: 'a', vmid: 1 }); const h = getInstanceHistory(1); expect(h).toHaveLength(1); expect(h[0].field).toBe('created'); }); it('logs changed fields when an instance is updated', () => { createInstance({ ...base, name: 'a', vmid: 1 }); updateInstance(1, { ...base, name: 'a', vmid: 1, state: 'degraded' }); const h = getInstanceHistory(1); const stateEvt = h.find(e => e.field === 'state'); expect(stateEvt).toBeDefined(); expect(stateEvt.old_value).toBe('deployed'); expect(stateEvt.new_value).toBe('degraded'); }); it('logs no events when nothing changes on update', () => { createInstance({ ...base, name: 'a', vmid: 1 }); updateInstance(1, { ...base, name: 'a', vmid: 1 }); const h = getInstanceHistory(1).filter(e => e.field !== 'created'); expect(h).toHaveLength(0); }); it('records history under the new vmid when vmid changes', () => { createInstance({ ...base, name: 'a', vmid: 1 }); updateInstance(1, { ...base, name: 'a', vmid: 2 }); expect(getInstanceHistory(2).some(e => e.field === 'vmid')).toBe(true); expect(getInstanceHistory(1).filter(e => e.field !== 'created')).toHaveLength(0); }); }); // ── Test environment boot isolation ─────────────────────────────────────────── describe('test environment boot isolation', () => { it('vitest runs with NODE_ENV=test', () => { // Vitest sets NODE_ENV=test automatically. This is the guard condition // that prevents the boot init() from opening the real database file. expect(process.env.NODE_ENV).toBe('test'); }); it('db module loads cleanly in parallel workers without locking the real db file', () => { // Regression: the module-level init(DEFAULT_PATH) used to run unconditionally, // causing "database is locked" when multiple test workers imported db.js at // the same time. process.exit(1) then killed the worker mid-suite. // Fix: boot init is skipped when NODE_ENV=test. _resetForTest() handles setup. // Reaching this line proves the module loaded without calling process.exit. expect(() => _resetForTest()).not.toThrow(); expect(getInstances()).toEqual([]); }); });