fix notion setup picker exit

This commit is contained in:
Andrey Avtomonov 2026-05-13 14:59:26 +02:00
parent 496cc3a767
commit affbba54d8
6 changed files with 40 additions and 3 deletions

View file

@ -378,7 +378,7 @@ describe('renderNotionPickerTui', () => {
},
),
).resolves.toEqual({ kind: 'quit' });
expect(stderr).toContain('Use --no-input --root-page-id <UUID> for scripted mode');
expect(stderr).toContain('Use --no-input --notion-root-page-id <UUID> for scripted mode');
expect(stderr).not.toContain('secret');
});
});

View file

@ -331,7 +331,7 @@ export async function renderNotionPickerTui(
return result;
} catch (error) {
io.stderr.write(
`Notion picker requires a TTY. Use --no-input --root-page-id <UUID> for scripted mode. ${sanitizeNotionPickerTuiError(error)}\n`,
`Notion picker requires a TTY. Use --no-input --notion-root-page-id <UUID> for scripted mode. ${sanitizeNotionPickerTuiError(error)}\n`,
);
return { kind: 'quit' };
}

View file

@ -17,9 +17,11 @@ function makeTracker(ctrlCValues: boolean[]): SetupInterruptTracker {
describe('setup interrupt confirmation', () => {
const originalIsTTY = process.stdin.isTTY;
const originalRef = process.stdin.ref;
afterEach(() => {
Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: originalIsTTY });
Object.defineProperty(process.stdin, 'ref', { configurable: true, value: originalRef });
});
it('fails before opening a prompt when interactive setup has no tty', async () => {
@ -33,6 +35,26 @@ describe('setup interrupt confirmation', () => {
expect(prompt).not.toHaveBeenCalled();
});
it('refs stdin before opening a real interactive prompt', async () => {
const calls: string[] = [];
Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: true });
Object.defineProperty(process.stdin, 'ref', {
configurable: true,
value: vi.fn(() => {
calls.push('ref');
return process.stdin;
}),
});
const prompt = vi.fn(async () => {
calls.push('prompt');
return 'continued';
});
await expect(withSetupInterruptConfirmation(prompt)).resolves.toBe('continued');
expect(calls).toEqual(['ref', 'prompt']);
});
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);

View file

@ -23,6 +23,10 @@ interface SetupInterruptOptions {
const NON_INTERACTIVE_SETUP_MESSAGE =
'Interactive setup requires a terminal. Re-run this command in a TTY, or pass --no-input with the required options.';
function refSetupInput(input: NodeJS.ReadStream = stdin): void {
input.ref?.();
}
function createSetupInterruptTracker(input: NodeJS.ReadStream = stdin): SetupInterruptTracker {
let ctrlCPressed = false;
const onKeypress = (char: string | undefined, key: Key) => {
@ -73,6 +77,9 @@ export async function withSetupInterruptConfirmation<T>(
const confirmExit = options.confirmExit ?? defaultConfirmExit;
while (true) {
if (!options.tracker) {
refSetupInput();
}
const value = await tracker.track(prompt);
if (!isCancel(value)) {
return value;

View file

@ -311,6 +311,14 @@ describe('setup sources step', () => {
).resolves.toEqual({ status: 'ready', projectDir, connectionIds: ['notion-main'] });
expect(pickNotionRootPages).toHaveBeenCalledOnce();
expect(testPrompts.select).toHaveBeenCalledWith({
message: 'Which Notion pages should KTX ingest?',
options: [
{ value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
{ value: 'all_accessible', label: 'All pages the integration can access' },
{ value: 'back', label: 'Back' },
],
});
expect((await readConfig()).connections['notion-main']).toMatchObject({
driver: 'notion',
auth_token_ref: 'env:NOTION_TOKEN',

View file

@ -1270,7 +1270,7 @@ async function promptForInteractiveSource(
const crawlMode = await prompts.select({
message: 'Which Notion pages should KTX ingest?',
options: [
{ value: 'selected_roots', label: 'Specific pages and their subpages (you\'ll paste page IDs)' },
{ value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
{ value: 'all_accessible', label: 'All pages the integration can access' },
{ value: 'back', label: 'Back' },
],