mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 19:57:37 +00:00
79914fe750
* fix: recognize clean one-shot completions in health gate and drift Treat exit 0 with restart policy no/absent as successful completion so init and migration jobs no longer fail post-update observation or show as service-missing, while long-running restart policies still fail closed. * fix: ignore residual health on clean one-shots and honor deploy.restart_policy Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots. * fix: require explicit Compose restart no for one-shot recognition Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate.
93 lines
3.9 KiB
TypeScript
93 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
isCleanOneShotCompletion,
|
|
isNoRestartPolicy,
|
|
normalizeComposeRestartIntent,
|
|
} from '../utils/oneShotCompletion';
|
|
|
|
describe('isNoRestartPolicy', () => {
|
|
it('accepts only explicit no', () => {
|
|
expect(isNoRestartPolicy('no')).toBe(true);
|
|
});
|
|
|
|
it('rejects absent, empty, and restarting policies', () => {
|
|
expect(isNoRestartPolicy(undefined)).toBe(false);
|
|
expect(isNoRestartPolicy(null)).toBe(false);
|
|
expect(isNoRestartPolicy('')).toBe(false);
|
|
expect(isNoRestartPolicy('unless-stopped')).toBe(false);
|
|
expect(isNoRestartPolicy('always')).toBe(false);
|
|
expect(isNoRestartPolicy('on-failure')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isCleanOneShotCompletion', () => {
|
|
const clean = {
|
|
state: 'exited',
|
|
exitCode: 0 as number | null,
|
|
restartPolicy: 'no' as string | null | undefined,
|
|
};
|
|
|
|
it('returns true only for exited + exit 0 + explicit restart no', () => {
|
|
expect(isCleanOneShotCompletion(clean)).toBe(true);
|
|
});
|
|
|
|
it('returns false for absent or empty declared restart', () => {
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: undefined })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: null })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: '' })).toBe(false);
|
|
});
|
|
|
|
it('returns false for non-exited states', () => {
|
|
expect(isCleanOneShotCompletion({ ...clean, state: 'running' })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, state: 'restarting' })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, state: 'created' })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, state: 'dead' })).toBe(false);
|
|
});
|
|
|
|
it('returns false for non-zero and null exit codes (fail closed)', () => {
|
|
expect(isCleanOneShotCompletion({ ...clean, exitCode: 1 })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, exitCode: 137 })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, exitCode: null })).toBe(false);
|
|
});
|
|
|
|
it('returns false for restarting policies even with exit 0', () => {
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: 'unless-stopped' })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: 'always' })).toBe(false);
|
|
expect(isCleanOneShotCompletion({ ...clean, restartPolicy: 'on-failure' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('normalizeComposeRestartIntent', () => {
|
|
it('falls back to service restart when deploy.restart_policy is unset', () => {
|
|
expect(normalizeComposeRestartIntent('no')).toBe('no');
|
|
expect(normalizeComposeRestartIntent('unless-stopped')).toBe('unless-stopped');
|
|
expect(normalizeComposeRestartIntent(undefined)).toBeNull();
|
|
expect(normalizeComposeRestartIntent(null, {})).toBeNull();
|
|
expect(normalizeComposeRestartIntent('always', { replicas: 2 })).toBe('always');
|
|
});
|
|
|
|
it('maps deploy.restart_policy.condition with Compose defaults and precedence', () => {
|
|
expect(normalizeComposeRestartIntent('unless-stopped', {
|
|
restart_policy: { condition: 'none' },
|
|
})).toBe('no');
|
|
expect(normalizeComposeRestartIntent(null, {
|
|
restart_policy: { condition: 'any' },
|
|
})).toBe('always');
|
|
expect(normalizeComposeRestartIntent('no', {
|
|
restart_policy: { condition: 'on-failure' },
|
|
})).toBe('on-failure');
|
|
expect(normalizeComposeRestartIntent('no', {
|
|
restart_policy: {},
|
|
})).toBe('always');
|
|
});
|
|
|
|
it('fails closed on malformed restart_policy shapes', () => {
|
|
expect(normalizeComposeRestartIntent('no', { restart_policy: null })).toBe('always');
|
|
expect(normalizeComposeRestartIntent('no', { restart_policy: 'none' })).toBe('always');
|
|
expect(normalizeComposeRestartIntent('no', { restart_policy: [] })).toBe('always');
|
|
expect(normalizeComposeRestartIntent('no', {
|
|
restart_policy: { condition: 'weird' },
|
|
})).toBe('always');
|
|
});
|
|
});
|