92 lines
3.8 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|