ktx/packages/cli/src/setup-interrupt.test.ts
2026-05-10 23:51:24 +02:00

90 lines
2.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
KtxSetupExitError,
withSetupInterruptConfirmation,
type SetupInterruptTracker,
} from './setup-interrupt.js';
const CANCEL = Symbol('cancel');
function makeTracker(ctrlCValues: boolean[]): SetupInterruptTracker {
return {
track: vi.fn((run) => run()),
wasCtrlC: vi.fn(() => ctrlCValues.shift() ?? false),
};
}
describe('setup interrupt confirmation', () => {
const originalIsTTY = process.stdin.isTTY;
afterEach(() => {
Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: originalIsTTY });
});
it('fails before opening a prompt when interactive setup has no tty', async () => {
Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: false });
const prompt = vi.fn(async () => 'continued');
await expect(withSetupInterruptConfirmation(prompt)).rejects.toThrow(
'Interactive setup requires a terminal. Re-run this command in a TTY, or pass --no-input with the required options.',
);
expect(prompt).not.toHaveBeenCalled();
});
it('asks before exiting on Ctrl+C and reruns the active prompt when declined', async () => {
const prompt = vi.fn(async () => (prompt.mock.calls.length === 1 ? CANCEL : 'continued'));
const confirmExit = vi.fn(async () => false);
await expect(
withSetupInterruptConfirmation(prompt, {
confirmExit,
isCancel: (value): value is symbol => value === CANCEL,
tracker: makeTracker([true]),
}),
).resolves.toBe('continued');
expect(prompt).toHaveBeenCalledTimes(2);
expect(confirmExit).toHaveBeenCalledTimes(1);
});
it('exits immediately when the confirmation is accepted', async () => {
const prompt = vi.fn(async () => CANCEL);
await expect(
withSetupInterruptConfirmation(prompt, {
confirmExit: vi.fn(async () => true),
isCancel: (value): value is symbol => value === CANCEL,
tracker: makeTracker([true]),
}),
).rejects.toBeInstanceOf(KtxSetupExitError);
});
it('keeps non-Ctrl+C cancellation available for Back and Escape flows', async () => {
const prompt = vi.fn(async () => CANCEL);
const confirmExit = vi.fn(async () => true);
await expect(
withSetupInterruptConfirmation(prompt, {
confirmExit,
isCancel: (value): value is symbol => value === CANCEL,
tracker: makeTracker([false]),
}),
).resolves.toBe(CANCEL);
expect(confirmExit).not.toHaveBeenCalled();
});
it('exits immediately when Ctrl+C is pressed again at the confirmation prompt', async () => {
const prompt = vi.fn(async () => CANCEL);
await expect(
withSetupInterruptConfirmation(prompt, {
confirmExit: vi.fn(async () => CANCEL),
isCancel: (value): value is symbol => value === CANCEL,
tracker: makeTracker([true]),
}),
).rejects.toBeInstanceOf(KtxSetupExitError);
});
});