Files
dockhand-upstream/tests/backups/operations-core.test.ts
T

92 lines
3.8 KiB
TypeScript

/**
* Unit tests for backups/operations-core.ts — the pure decision logic of the
* operation record: terminal-status derivation, the startup scrub rule, and the
* progress throttle.
*/
import { describe, it, expect } from 'bun:test';
import {
deriveTerminalState,
toExecutionStatus,
shouldWriteProgress,
isAlwaysPersistedStatus,
ProgressThrottle,
type Outcome,
} from '../../src/lib/server/backups/operations-core';
describe('deriveTerminalState — one place decides the terminal status', () => {
it('ok → success', () => {
expect(deriveTerminalState({ kind: 'ok' })).toEqual({ status: 'success' });
});
it('warning → warning with the message', () => {
expect(deriveTerminalState({ kind: 'warning', message: 'partial read' }))
.toEqual({ status: 'warning', message: 'partial read' });
});
it('cancelled → cancelled (default message)', () => {
expect(deriveTerminalState({ kind: 'cancelled' })).toEqual({ status: 'cancelled', message: 'Cancelled' });
expect(deriveTerminalState({ kind: 'cancelled', message: 'by user' }))
.toEqual({ status: 'cancelled', message: 'by user' });
});
it('error → error with message + code', () => {
const o: Outcome = { kind: 'error', code: 'INTEGRITY', message: 'snapshot unreadable' };
expect(deriveTerminalState(o)).toEqual({ status: 'error', message: 'snapshot unreadable', errorCode: 'INTEGRITY' });
});
});
describe('toExecutionStatus — engine vocab → shared executions-table vocab', () => {
it('error → failed (the schedules UI reads "failed", not the engine-internal "error")', () => {
expect(toExecutionStatus('error')).toBe('failed');
});
it('cancelled → failed (no "cancelled" in the executions vocab)', () => {
expect(toExecutionStatus('cancelled')).toBe('failed');
});
it('success/warning pass through', () => {
expect(toExecutionStatus('success')).toBe('success');
expect(toExecutionStatus('warning')).toBe('warning');
});
it('a skipped-flagged close maps to skipped regardless of the terminal status', () => {
expect(toExecutionStatus('error', { skipped: true })).toBe('skipped');
expect(toExecutionStatus('success', { skipped: true })).toBe('skipped');
});
});
describe('shouldWriteProgress — first write immediate, then throttled', () => {
it('always writes the first update (lastWrittenAt null)', () => {
expect(shouldWriteProgress(null, 1000, 1000)).toBe(true);
});
it('suppresses a write within the interval', () => {
expect(shouldWriteProgress(1000, 1500, 1000)).toBe(false);
});
it('allows a write once the interval has elapsed', () => {
expect(shouldWriteProgress(1000, 2000, 1000)).toBe(true);
expect(shouldWriteProgress(1000, 2001, 1000)).toBe(true);
});
});
describe('ProgressThrottle — stateful, clock injected', () => {
it('writes first, throttles within interval, writes again after', () => {
let t = 0;
const th = new ProgressThrottle(1000, () => t);
expect(th.shouldWrite()).toBe(true); // t=0, first
t = 500;
expect(th.shouldWrite()).toBe(false); // within 1000
t = 1000;
expect(th.shouldWrite()).toBe(true); // interval elapsed
t = 1200;
expect(th.shouldWrite()).toBe(false); // within next interval
});
});
describe('isAlwaysPersistedStatus — only restic spam is throttled', () => {
it("throttles ONLY the high-frequency restic stream (status 'progress')", () => {
expect(isAlwaysPersistedStatus('progress')).toBe(false);
});
it('ALWAYS persists every meaningful low-frequency event', () => {
// Stage markers, the volume list, lifecycle steps, errors/warnings must never
// drop out of the execution log when they collide with restic spam.
for (const s of ['metadata', 'backing-up', 'verifying', 'pruning', 'restoring',
'recreating', 'redeploying', 'starting', 'stopping', 'restarted', 'error', 'warning']) {
expect(isAlwaysPersistedStatus(s)).toBe(true);
}
});
});