175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
getDeferredRestartAction,
|
|
getReconnectScheduleDecision,
|
|
getReconnectSkipReason,
|
|
isLifecycleSuperseded,
|
|
nextLifecycleEpoch,
|
|
shouldDeferRestart,
|
|
} from '@electron/gateway/process-policy';
|
|
|
|
describe('gateway process policy helpers', () => {
|
|
describe('lifecycle epoch helpers', () => {
|
|
it('increments lifecycle epoch by one', () => {
|
|
expect(nextLifecycleEpoch(0)).toBe(1);
|
|
expect(nextLifecycleEpoch(5)).toBe(6);
|
|
});
|
|
|
|
it('detects superseded lifecycle epochs', () => {
|
|
expect(isLifecycleSuperseded(3, 4)).toBe(true);
|
|
expect(isLifecycleSuperseded(8, 8)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getReconnectSkipReason', () => {
|
|
it('skips reconnect when auto-reconnect is disabled', () => {
|
|
expect(
|
|
getReconnectSkipReason({
|
|
scheduledEpoch: 10,
|
|
currentEpoch: 10,
|
|
shouldReconnect: false,
|
|
})
|
|
).toBe('auto-reconnect disabled');
|
|
});
|
|
|
|
it('skips stale reconnect callbacks when lifecycle epoch changed', () => {
|
|
expect(
|
|
getReconnectSkipReason({
|
|
scheduledEpoch: 11,
|
|
currentEpoch: 12,
|
|
shouldReconnect: true,
|
|
})
|
|
).toContain('stale reconnect callback');
|
|
});
|
|
|
|
it('allows reconnect when callback is current and reconnect enabled', () => {
|
|
expect(
|
|
getReconnectSkipReason({
|
|
scheduledEpoch: 7,
|
|
currentEpoch: 7,
|
|
shouldReconnect: true,
|
|
})
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('restart deferral policy', () => {
|
|
it('defers restart while startup or reconnect is in progress', () => {
|
|
expect(shouldDeferRestart({ state: 'starting', startLock: false })).toBe(true);
|
|
expect(shouldDeferRestart({ state: 'reconnecting', startLock: false })).toBe(true);
|
|
expect(shouldDeferRestart({ state: 'running', startLock: true })).toBe(true);
|
|
});
|
|
|
|
it('does not defer restart for stable states when no start lock', () => {
|
|
expect(shouldDeferRestart({ state: 'running', startLock: false })).toBe(false);
|
|
expect(shouldDeferRestart({ state: 'stopped', startLock: false })).toBe(false);
|
|
expect(shouldDeferRestart({ state: 'error', startLock: false })).toBe(false);
|
|
});
|
|
|
|
it('executes deferred restart even after lifecycle recovers to running', () => {
|
|
expect(
|
|
getDeferredRestartAction({
|
|
hasPendingRestart: true,
|
|
state: 'running',
|
|
startLock: false,
|
|
shouldReconnect: true,
|
|
})
|
|
).toBe('execute');
|
|
});
|
|
|
|
it('waits deferred restart while lifecycle is still busy', () => {
|
|
expect(
|
|
getDeferredRestartAction({
|
|
hasPendingRestart: true,
|
|
state: 'starting',
|
|
startLock: false,
|
|
shouldReconnect: true,
|
|
})
|
|
).toBe('wait');
|
|
});
|
|
|
|
it('executes deferred restart when manager is idle and not running', () => {
|
|
expect(
|
|
getDeferredRestartAction({
|
|
hasPendingRestart: true,
|
|
state: 'error',
|
|
startLock: false,
|
|
shouldReconnect: true,
|
|
})
|
|
).toBe('execute');
|
|
});
|
|
|
|
it('drops deferred restart when reconnect is disabled', () => {
|
|
expect(
|
|
getDeferredRestartAction({
|
|
hasPendingRestart: true,
|
|
state: 'stopped',
|
|
startLock: false,
|
|
shouldReconnect: false,
|
|
})
|
|
).toBe('drop');
|
|
});
|
|
});
|
|
|
|
describe('getReconnectScheduleDecision', () => {
|
|
const baseContext = {
|
|
shouldReconnect: true,
|
|
hasReconnectTimer: false,
|
|
reconnectAttempts: 0,
|
|
maxAttempts: 10,
|
|
baseDelay: 1000,
|
|
maxDelay: 30000,
|
|
};
|
|
|
|
it('skips reconnect when shouldReconnect is false (intentional stop)', () => {
|
|
const decision = getReconnectScheduleDecision({
|
|
...baseContext,
|
|
shouldReconnect: false,
|
|
});
|
|
expect(decision).toEqual({ action: 'skip', reason: 'auto-reconnect disabled' });
|
|
});
|
|
|
|
it('returns already-scheduled when a reconnect timer exists (prevents double-scheduling)', () => {
|
|
const decision = getReconnectScheduleDecision({
|
|
...baseContext,
|
|
hasReconnectTimer: true,
|
|
});
|
|
expect(decision).toEqual({ action: 'already-scheduled' });
|
|
});
|
|
|
|
it('fails when max reconnect attempts are exhausted', () => {
|
|
const decision = getReconnectScheduleDecision({
|
|
...baseContext,
|
|
reconnectAttempts: 10,
|
|
maxAttempts: 10,
|
|
});
|
|
expect(decision).toEqual({ action: 'fail', attempts: 10, maxAttempts: 10 });
|
|
});
|
|
|
|
it('schedules reconnect with exponential backoff delay', () => {
|
|
const decision = getReconnectScheduleDecision({
|
|
...baseContext,
|
|
reconnectAttempts: 0,
|
|
});
|
|
expect(decision).toEqual({
|
|
action: 'schedule',
|
|
nextAttempt: 1,
|
|
maxAttempts: 10,
|
|
delay: 1000,
|
|
});
|
|
});
|
|
|
|
it('caps backoff delay at maxDelay', () => {
|
|
const decision = getReconnectScheduleDecision({
|
|
...baseContext,
|
|
reconnectAttempts: 8,
|
|
maxDelay: 30000,
|
|
});
|
|
expect(decision).toMatchObject({ action: 'schedule' });
|
|
if (decision.action === 'schedule') {
|
|
expect(decision.delay).toBeLessThanOrEqual(30000);
|
|
}
|
|
});
|
|
});
|
|
});
|