Tests cover: - sort by vmid asc/desc - sort by name desc - sort by created_at asc/desc (id tiebreaker for same-second inserts) - sort by updated_at asc/desc (id tiebreaker for same-second inserts) - invalid sort field falls back to name asc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
454 lines
23 KiB
JavaScript
454 lines
23 KiB
JavaScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import {
|
|
_resetForTest,
|
|
getInstances, getInstance, getDistinctStacks,
|
|
createInstance, updateInstance, deleteInstance, importInstances, getInstanceHistory,
|
|
getConfig, setConfig,
|
|
getJobs, getJob, createJob, updateJob, createJobRun, completeJobRun, getJobRuns,
|
|
} 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);
|
|
});
|
|
|
|
it('sorts by vmid ascending', () => {
|
|
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 });
|
|
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 100, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
|
const result = getInstances({ sort: 'vmid' });
|
|
expect(result[0].vmid).toBe(100);
|
|
expect(result[1].vmid).toBe(200);
|
|
});
|
|
|
|
it('sorts by vmid descending', () => {
|
|
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 100, 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 });
|
|
const result = getInstances({ sort: 'vmid', order: 'desc' });
|
|
expect(result[0].vmid).toBe(200);
|
|
expect(result[1].vmid).toBe(100);
|
|
});
|
|
|
|
it('sorts by name descending', () => {
|
|
createInstance({ name: 'alpha', 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: 'zebra', 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({ sort: 'name', order: 'desc' });
|
|
expect(result[0].name).toBe('zebra');
|
|
expect(result[1].name).toBe('alpha');
|
|
});
|
|
|
|
it('sorts by created_at asc — id is tiebreaker when timestamps are equal (same second)', () => {
|
|
// datetime('now') has second precision; rapid inserts share the same timestamp.
|
|
// The implementation uses id ASC as secondary sort so insertion order is preserved.
|
|
createInstance({ name: 'first', 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: 'second', 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({ sort: 'created_at', order: 'asc' });
|
|
expect(result[0].name).toBe('first'); // id=1 before id=2
|
|
expect(result[1].name).toBe('second');
|
|
});
|
|
|
|
it('sorts by created_at desc — id is tiebreaker when timestamps are equal (same second)', () => {
|
|
createInstance({ name: 'first', 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: 'second', 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({ sort: 'created_at', order: 'desc' });
|
|
expect(result[0].name).toBe('second'); // id=2 before id=1
|
|
expect(result[1].name).toBe('first');
|
|
});
|
|
|
|
it('sorts by updated_at asc — id is tiebreaker when timestamps are equal (same second)', () => {
|
|
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: '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({ sort: 'updated_at', order: 'asc' });
|
|
expect(result[0].name).toBe('a'); // id=1 before id=2
|
|
expect(result[1].name).toBe('b');
|
|
});
|
|
|
|
it('sorts by updated_at desc — id is tiebreaker when timestamps are equal (same second)', () => {
|
|
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: '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({ sort: 'updated_at', order: 'desc' });
|
|
expect(result[0].name).toBe('b'); // id=2 before id=1
|
|
expect(result[1].name).toBe('a');
|
|
});
|
|
|
|
it('falls back to name asc for an invalid sort field', () => {
|
|
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({ sort: 'injected; DROP TABLE instances--' });
|
|
expect(result[0].name).toBe('alpha');
|
|
});
|
|
});
|
|
|
|
// ── 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();
|
|
});
|
|
|
|
it('clears history for the deleted instance', () => {
|
|
createInstance({ ...base, name: 'a', vmid: 1 });
|
|
deleteInstance(1);
|
|
expect(getInstanceHistory(1)).toHaveLength(0);
|
|
});
|
|
|
|
it('does not clear history for other instances', () => {
|
|
createInstance({ ...base, name: 'a', vmid: 1 });
|
|
createInstance({ ...base, name: 'b', vmid: 2 });
|
|
deleteInstance(1);
|
|
expect(getInstanceHistory(2).length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ── 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([]);
|
|
});
|
|
|
|
it('clears history for all replaced instances', () => {
|
|
createInstance({ ...base, name: 'old', vmid: 1 });
|
|
importInstances([{ ...base, name: 'new', vmid: 2 }]);
|
|
expect(getInstanceHistory(1)).toHaveLength(0);
|
|
});
|
|
|
|
it('restores history rows when provided', () => {
|
|
importInstances(
|
|
[{ ...base, name: 'a', vmid: 1 }],
|
|
[{ vmid: 1, field: 'created', old_value: null, new_value: null, changed_at: '2026-01-01 00:00:00' }]
|
|
);
|
|
const h = getInstanceHistory(1);
|
|
expect(h.some(e => e.field === 'created')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── 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([]);
|
|
});
|
|
});
|
|
|
|
// ── getConfig / setConfig ─────────────────────────────────────────────────────
|
|
|
|
describe('getConfig / setConfig', () => {
|
|
it('returns defaultVal when key does not exist', () => {
|
|
expect(getConfig('missing', 'fallback')).toBe('fallback');
|
|
});
|
|
|
|
it('returns empty string by default', () => {
|
|
expect(getConfig('missing')).toBe('');
|
|
});
|
|
|
|
it('stores and retrieves a value', () => {
|
|
setConfig('tailscale_api_key', 'tskey-test');
|
|
expect(getConfig('tailscale_api_key')).toBe('tskey-test');
|
|
});
|
|
|
|
it('overwrites an existing key', () => {
|
|
setConfig('tailscale_enabled', '0');
|
|
setConfig('tailscale_enabled', '1');
|
|
expect(getConfig('tailscale_enabled')).toBe('1');
|
|
});
|
|
|
|
it('config is cleared by _resetForTest', () => {
|
|
setConfig('tailscale_api_key', 'tskey-test');
|
|
_resetForTest();
|
|
expect(getConfig('tailscale_api_key')).toBe('');
|
|
});
|
|
});
|
|
|
|
// ── jobs ──────────────────────────────────────────────────────────────────────
|
|
|
|
const baseJob = {
|
|
key: 'test_job', name: 'Test Job', description: 'desc',
|
|
enabled: 0, schedule: 15, config: '{}',
|
|
};
|
|
|
|
describe('jobs', () => {
|
|
it('returns empty array when no jobs', () => {
|
|
expect(getJobs()).toEqual([]);
|
|
});
|
|
|
|
it('createJob + getJobs returns the job', () => {
|
|
createJob(baseJob);
|
|
expect(getJobs()).toHaveLength(1);
|
|
expect(getJobs()[0].name).toBe('Test Job');
|
|
});
|
|
|
|
it('getJob returns null for unknown id', () => {
|
|
expect(getJob(999)).toBeNull();
|
|
});
|
|
|
|
it('updateJob changes enabled and schedule', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
updateJob(id, { enabled: 1, schedule: 30, config: '{}' });
|
|
expect(getJob(id).enabled).toBe(1);
|
|
expect(getJob(id).schedule).toBe(30);
|
|
});
|
|
|
|
it('getJobs includes last_status null when no runs', () => {
|
|
createJob(baseJob);
|
|
expect(getJobs()[0].last_status).toBeNull();
|
|
});
|
|
|
|
it('getJobs reflects last_status after a run', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
const runId = createJobRun(id);
|
|
completeJobRun(runId, 'success', 'ok');
|
|
expect(getJobs()[0].last_status).toBe('success');
|
|
});
|
|
});
|
|
|
|
// ── job_runs ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('job_runs', () => {
|
|
it('createJobRun returns a positive id', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
expect(createJobRun(id)).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('new run has status running and no ended_at', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
const runId = createJobRun(id);
|
|
const runs = getJobRuns(id);
|
|
expect(runs[0].status).toBe('running');
|
|
expect(runs[0].ended_at).toBeNull();
|
|
});
|
|
|
|
it('completeJobRun sets status, result, and ended_at', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
const runId = createJobRun(id);
|
|
completeJobRun(runId, 'success', '2 updated of 8');
|
|
const run = getJobRuns(id)[0];
|
|
expect(run.status).toBe('success');
|
|
expect(run.result).toBe('2 updated of 8');
|
|
expect(run.ended_at).not.toBeNull();
|
|
});
|
|
|
|
it('getJobRuns returns newest first', () => {
|
|
createJob(baseJob);
|
|
const id = getJobs()[0].id;
|
|
const r1 = createJobRun(id);
|
|
const r2 = createJobRun(id);
|
|
completeJobRun(r1, 'success', 'first');
|
|
completeJobRun(r2, 'error', 'second');
|
|
const runs = getJobRuns(id);
|
|
expect(runs[0].id).toBe(r2);
|
|
expect(runs[1].id).toBe(r1);
|
|
});
|
|
});
|