mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
* Codex worktree snapshot: startup-cleanup Co-authored-by: Codex * Add Python SDK real smoke test Adds a repository-only real E2E smoke script for the Python SDK, plus npm and developer documentation entry points. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address review findings — bugs, type safety, and test coverage - Fix prepare_spawn_info: JS files now use "node" instead of sys.executable - Fix protocol.py: correct total=False misuse on 7 TypedDicts (required fields were optional) - Fix query.py: add _closed guard in _ensure_started, suppress exceptions in close() - Fix sync_query.py: prevent close() deadlock, add context manager, add timeouts - Fix transport.py: handle malformed JSON lines, add _closed guard in start() - Fix validation.py: use uuid.RFC_4122 instead of magic UUID - Fix __init__.py: export TextBlock, widen query_sync signature - Remove dead code: ensure_not_aborted, write_json_line, _thread_error - Add 12 new tests (29 → 41): context managers, JSON skip, closed guards, spawn info, timeouts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address wenshao review — session_id, bool validation, debug stderr - Fix continue_session=True generating a wrong random session_id - Add _as_optional_bool helper for strict type validation on bool fields - Default debug stderr to sys.stderr when no custom callback is provided Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address remaining wenshao review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): harden settings dialog restart prompt test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — UUID compat, stderr fallback, sync cleanup - Remove UUID version restriction to support v6/v7/v8 (RFC 9562) - Always write to sys.stderr when stderr callback raises (was silent when debug=False) - Prevent duplicate _STOP sentinel in SyncQuery.close() via _stop_sent flag - Add ruff format --check to CI workflow - Fix smoke_real.py version guard: fail early before imports instead of NameError - Apply ruff format to existing files Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): remaining review fixes — exit_code attr, guard strictness, sync timeout - Add exit_code attribute to ProcessExitError for programmatic access - Strengthen is_control_response/is_control_cancel guards to require payload fields, preventing misrouting of malformed messages - Expose control_request_timeout property on Query so SyncQuery uses the configured timeout instead of a hardcoded 30s default - Use dataclasses.replace() instead of direct mutation on frozen-style QueryOptions in query() factory - Add ResourceWarning in SyncQuery.__del__ when not properly closed Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): add exit_code default and guard __del__ against partial GC - Give ProcessExitError.exit_code a default value (-1) so user code can construct the exception with just a message string - Wrap SyncQuery.__del__ in try/except AttributeError to prevent crashes when the object is partially garbage-collected Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — resource leak, type safety, CI matrix, docs - Fix SyncQuery.__del__ to call close() on GC instead of only warning - Replace hasattr duck-type check with isinstance(prompt, AsyncIterable) - Type-validate permission_mode/auth_type in QueryOptions.from_mapping - Use TypeGuard return types on all is_sdk_*/is_control_* predicates - Add 5s margin to sync wrapper timeouts to prevent error type masking - Expand CI matrix to test Python 3.10, 3.11, 3.12 - Change ProcessExitError.exit_code default from -1 to None - Add stderr to docs QueryOptions listing - Update README sync example to use context manager pattern Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): preserve iterator exhaustion state and suppress detached task warning - Add _exhausted flag to Query.__anext__ and SyncQuery.__next__ so repeated iteration after end-of-stream raises Stop(Async)Iteration instead of blocking forever. - Remove re-raise in _initialize() to prevent asyncio "Task exception was never retrieved" warning on detached tasks; the error is already surfaced via _finish_with_error(). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): reject mcp_servers at validation time and add iterator/init tests - Reject mcp_servers in validate_query_options() with a clear error instead of advertising MCP support to the CLI and then failing at runtime when mcp_message arrives. - Remove dead mcp_servers branch from _initialize(). - Add tests for async/sync iterator exhaustion, detached init task warning suppression, and mcp_servers validation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): fix ruff lint errors in new tests - Use ControlRequestTimeoutError instead of bare Exception (B017) - Fix import sorting for stdlib vs third-party (I001) - Break long line to stay within 88-char limit (E501) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * style(sdk-python): apply ruff format to new tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1729 lines
51 KiB
TypeScript
1729 lines
51 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
*
|
|
*
|
|
* This test suite covers:
|
|
* - Initial rendering and display state
|
|
* - Keyboard navigation (arrows, vim keys, Tab)
|
|
* - Settings toggling (Enter, Space)
|
|
* - Focus section switching between settings and scope selector
|
|
* - Scope selection and settings persistence across scopes
|
|
* - Restart-required vs immediate settings behavior
|
|
* - VimModeContext integration
|
|
* - Complex user interaction workflows
|
|
* - Error handling and edge cases
|
|
* - Display values for inherited and overridden settings
|
|
*
|
|
*/
|
|
|
|
import { render } from 'ink-testing-library';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { SettingsDialog } from './SettingsDialog.js';
|
|
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
|
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
|
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
|
import { act } from 'react';
|
|
import {
|
|
getDialogSettingKeys,
|
|
getSettingDefinition,
|
|
saveModifiedSettings,
|
|
TEST_ONLY,
|
|
} from '../../utils/settingsUtils.js';
|
|
import { OUTPUT_LANGUAGE_AUTO } from '../../utils/languageUtils.js';
|
|
|
|
// Mock the VimModeContext
|
|
const mockToggleVimEnabled = vi.fn();
|
|
const mockSetVimMode = vi.fn();
|
|
|
|
// Mock the CompactModeContext
|
|
const mockSetCompactMode = vi.fn();
|
|
|
|
// Mock the UIActionsContext
|
|
const mockRefreshStatic = vi.fn();
|
|
|
|
enum TerminalKeys {
|
|
ENTER = '\u000D',
|
|
TAB = '\t',
|
|
UP_ARROW = '\u001B[A',
|
|
DOWN_ARROW = '\u001B[B',
|
|
LEFT_ARROW = '\u001B[D',
|
|
RIGHT_ARROW = '\u001B[C',
|
|
ESCAPE = '\u001B',
|
|
}
|
|
|
|
const createMockSettings = (
|
|
userSettings = {},
|
|
systemSettings = {},
|
|
workspaceSettings = {},
|
|
) =>
|
|
new LoadedSettings(
|
|
{
|
|
settings: { ui: { customThemes: {} }, mcpServers: {}, ...systemSettings },
|
|
originalSettings: {
|
|
ui: { customThemes: {} },
|
|
mcpServers: {},
|
|
...systemSettings,
|
|
},
|
|
path: '/system/settings.json',
|
|
},
|
|
{
|
|
settings: {},
|
|
originalSettings: {},
|
|
path: '/system/system-defaults.json',
|
|
},
|
|
{
|
|
settings: {
|
|
ui: { customThemes: {} },
|
|
mcpServers: {},
|
|
...userSettings,
|
|
},
|
|
originalSettings: {
|
|
ui: { customThemes: {} },
|
|
mcpServers: {},
|
|
...userSettings,
|
|
},
|
|
path: '/user/settings.json',
|
|
},
|
|
{
|
|
settings: {
|
|
ui: { customThemes: {} },
|
|
mcpServers: {},
|
|
...workspaceSettings,
|
|
},
|
|
originalSettings: {
|
|
ui: { customThemes: {} },
|
|
mcpServers: {},
|
|
...workspaceSettings,
|
|
},
|
|
path: '/workspace/settings.json',
|
|
},
|
|
true,
|
|
new Set(),
|
|
);
|
|
|
|
vi.mock('../../config/settingsSchema.js', async (importOriginal) => {
|
|
const original =
|
|
await importOriginal<typeof import('../../config/settingsSchema.js')>();
|
|
return {
|
|
...original,
|
|
getSettingsSchema: vi.fn(original.getSettingsSchema),
|
|
};
|
|
});
|
|
|
|
vi.mock('../contexts/VimModeContext.js', async () => {
|
|
const actual = await vi.importActual('../contexts/VimModeContext.js');
|
|
return {
|
|
...actual,
|
|
useVimMode: () => ({
|
|
vimEnabled: false,
|
|
vimMode: 'INSERT' as const,
|
|
toggleVimEnabled: mockToggleVimEnabled,
|
|
setVimMode: mockSetVimMode,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../contexts/CompactModeContext.js', async () => {
|
|
const actual = await vi.importActual('../contexts/CompactModeContext.js');
|
|
return {
|
|
...actual,
|
|
useCompactMode: () => ({
|
|
compactMode: false,
|
|
setCompactMode: mockSetCompactMode,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../contexts/UIActionsContext.js', async () => {
|
|
const actual = await vi.importActual('../contexts/UIActionsContext.js');
|
|
return {
|
|
...actual,
|
|
useUIActions: () => ({
|
|
refreshStatic: mockRefreshStatic,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../utils/settingsUtils.js', async () => {
|
|
const actual = await vi.importActual('../../utils/settingsUtils.js');
|
|
return {
|
|
...actual,
|
|
saveModifiedSettings: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../utils/languageUtils.js', async () => {
|
|
const actual = await vi.importActual('../../utils/languageUtils.js');
|
|
return {
|
|
...actual,
|
|
updateOutputLanguageFile: vi.fn(),
|
|
};
|
|
});
|
|
|
|
// Helper function to simulate key presses (commented out for now)
|
|
// const simulateKeyPress = async (keyData: Partial<Key> & { name: string }) => {
|
|
// if (currentKeypressHandler) {
|
|
// const key: Key = {
|
|
// ctrl: false,
|
|
// meta: false,
|
|
// shift: false,
|
|
// paste: false,
|
|
// sequence: keyData.sequence || keyData.name,
|
|
// ...keyData,
|
|
// };
|
|
// currentKeypressHandler(key);
|
|
// // Allow React to process the state update
|
|
// await new Promise(resolve => setTimeout(resolve, 10));
|
|
// }
|
|
// };
|
|
|
|
// Mock console.log to avoid noise in tests
|
|
// const originalConsoleLog = console.log;
|
|
// const originalConsoleError = console.error;
|
|
|
|
describe('SettingsDialog', () => {
|
|
// Simple delay function for remaining tests that need gradual migration
|
|
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
// Custom waitFor utility for ink testing environment (not compatible with @testing-library/react)
|
|
const waitFor = async (
|
|
predicate: () => void,
|
|
options: { timeout?: number; interval?: number } = {},
|
|
) => {
|
|
const { timeout = 1000, interval = 10 } = options;
|
|
const start = Date.now();
|
|
let lastError: unknown;
|
|
while (Date.now() - start < timeout) {
|
|
try {
|
|
predicate();
|
|
return;
|
|
} catch (e) {
|
|
lastError = e;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
}
|
|
if (lastError) {
|
|
throw lastError;
|
|
}
|
|
throw new Error('waitFor timed out');
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Reset keypress mock state (variables are commented out)
|
|
// currentKeypressHandler = null;
|
|
// isKeypressActive = false;
|
|
// console.log = vi.fn();
|
|
// console.error = vi.fn();
|
|
mockToggleVimEnabled.mockResolvedValue(true);
|
|
});
|
|
|
|
afterEach(() => {
|
|
TEST_ONLY.clearFlattenedSchema();
|
|
vi.clearAllMocks();
|
|
vi.resetAllMocks();
|
|
// Reset keypress mock state (variables are commented out)
|
|
// currentKeypressHandler = null;
|
|
// isKeypressActive = false;
|
|
// console.log = originalConsoleLog;
|
|
// console.error = originalConsoleError;
|
|
});
|
|
|
|
describe('Initial Rendering', () => {
|
|
it('should render the settings dialog with default state', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
expect(output).toContain('Settings');
|
|
// Scope selector is now in a separate view (Tab to switch)
|
|
expect(output).not.toContain('Apply To');
|
|
expect(output).toContain('(Use Enter to select, Tab to configure scope)');
|
|
});
|
|
|
|
it('should accept availableTerminalHeight prop without errors', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog
|
|
settings={settings}
|
|
onSelect={onSelect}
|
|
availableTerminalHeight={20}
|
|
/>
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// Should still render properly with the height prop
|
|
expect(output).toContain('Settings');
|
|
expect(output).toContain('Enter to select');
|
|
});
|
|
|
|
it('should show settings list with default values', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// Should show some default settings
|
|
expect(output).toContain('●'); // Active indicator
|
|
});
|
|
|
|
it('should highlight first setting by default', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// First item should be highlighted with green color and active indicator
|
|
expect(output).toContain('●');
|
|
});
|
|
});
|
|
|
|
describe('Settings Navigation', () => {
|
|
it('should navigate down with arrow key', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount, lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Press down arrow
|
|
act(() => {
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
|
});
|
|
|
|
const secondKey = getDialogSettingKeys()[1];
|
|
expect(secondKey).toBeDefined();
|
|
const secondLabel = secondKey
|
|
? (getSettingDefinition(secondKey)?.label ?? secondKey)
|
|
: '';
|
|
expect(lastFrame()).toContain(`● ${secondLabel}`);
|
|
|
|
// The active index should have changed (tested indirectly through behavior)
|
|
unmount();
|
|
});
|
|
|
|
it('should navigate up with arrow key', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// First go down, then up
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
|
await wait();
|
|
stdin.write(TerminalKeys.UP_ARROW as string);
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should navigate with vim keys (j/k)', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Navigate with vim keys
|
|
stdin.write('j'); // Down
|
|
await wait();
|
|
stdin.write('k'); // Up
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('wraps around when at the top of the list', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount, lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Try to go up from first item
|
|
act(() => {
|
|
stdin.write(TerminalKeys.UP_ARROW);
|
|
});
|
|
|
|
await wait();
|
|
|
|
const lastKey = getDialogSettingKeys().at(-1);
|
|
expect(lastKey).toBeDefined();
|
|
|
|
const lastLabel = lastKey
|
|
? (getSettingDefinition(lastKey)?.label ?? lastKey)
|
|
: '';
|
|
|
|
expect(lastFrame()).toContain(`● ${lastLabel}`);
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Settings Toggling', () => {
|
|
it('should toggle setting with Enter key', async () => {
|
|
vi.mocked(saveModifiedSettings).mockClear();
|
|
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
const component = (
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>
|
|
);
|
|
|
|
const { stdin, unmount, lastFrame } = render(component);
|
|
|
|
// Wait for initial render and verify we're on Tool Approval Mode (first setting)
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Tool Approval Mode');
|
|
});
|
|
|
|
const dialogKeys = getDialogSettingKeys();
|
|
const targetIndex = dialogKeys.indexOf('general.vimMode');
|
|
expect(targetIndex).toBeGreaterThan(0);
|
|
|
|
// Navigate to Vim Mode setting and verify we're there
|
|
for (let i = 0; i < targetIndex; i++) {
|
|
act(() => {
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
|
});
|
|
await wait();
|
|
}
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Vim Mode');
|
|
});
|
|
|
|
// Toggle the setting
|
|
act(() => {
|
|
stdin.write(TerminalKeys.ENTER as string);
|
|
});
|
|
// Wait for the setting change to be processed
|
|
await waitFor(() => {
|
|
expect(
|
|
vi.mocked(saveModifiedSettings).mock.calls.length,
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Wait for the mock to be called
|
|
await waitFor(() => {
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
|
new Set<string>(['general.vimMode']),
|
|
{
|
|
general: {
|
|
vimMode: true,
|
|
},
|
|
},
|
|
expect.any(LoadedSettings),
|
|
SettingScope.User,
|
|
);
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should sync compact mode with CompactModeContext when toggled', async () => {
|
|
vi.mocked(saveModifiedSettings).mockClear();
|
|
mockSetCompactMode.mockClear();
|
|
mockRefreshStatic.mockClear();
|
|
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
const component = (
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>
|
|
);
|
|
|
|
const { stdin, unmount, lastFrame } = render(component);
|
|
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Tool Approval Mode');
|
|
});
|
|
|
|
const dialogKeys = getDialogSettingKeys();
|
|
const targetIndex = dialogKeys.indexOf('ui.compactMode');
|
|
expect(targetIndex).toBeGreaterThan(0);
|
|
|
|
// Navigate to Compact Mode setting
|
|
for (let i = 0; i < targetIndex; i++) {
|
|
act(() => {
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
|
});
|
|
await wait();
|
|
}
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Compact Mode');
|
|
});
|
|
|
|
// Toggle the setting
|
|
act(() => {
|
|
stdin.write(TerminalKeys.ENTER as string);
|
|
});
|
|
await waitFor(() => {
|
|
expect(
|
|
vi.mocked(saveModifiedSettings).mock.calls.length,
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Verify compact mode context was synced
|
|
expect(mockSetCompactMode).toHaveBeenCalledWith(true);
|
|
// Verify refreshStatic was called to update rendered history
|
|
expect(mockRefreshStatic).toHaveBeenCalled();
|
|
|
|
unmount();
|
|
});
|
|
|
|
describe('enum values', () => {
|
|
it('toggles enum values with the enter key', async () => {
|
|
vi.mocked(saveModifiedSettings).mockClear();
|
|
|
|
// Use real schema - first setting "Tool Approval Mode" is an enum
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
const component = (
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>
|
|
);
|
|
|
|
const { stdin, unmount, lastFrame } = render(component);
|
|
|
|
// Verify we're on Tool Approval Mode (first setting, an enum)
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Tool Approval Mode');
|
|
});
|
|
|
|
// Press Enter to cycle the enum value
|
|
act(() => {
|
|
stdin.write(TerminalKeys.ENTER as string);
|
|
});
|
|
await wait();
|
|
await waitFor(() => {
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
|
});
|
|
|
|
// Tool Approval Mode cycles through enum values
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
|
new Set<string>(['tools.approvalMode']),
|
|
expect.objectContaining({
|
|
tools: expect.objectContaining({
|
|
approvalMode: expect.any(String),
|
|
}),
|
|
}),
|
|
expect.any(LoadedSettings),
|
|
SettingScope.User,
|
|
);
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('loops back when reaching the end of an enum', async () => {
|
|
vi.mocked(saveModifiedSettings).mockClear();
|
|
// Use Tool Approval Mode set to YOLO (last value) to test looping back to first
|
|
const settings = createMockSettings({
|
|
tools: {
|
|
approvalMode: 'yolo', // Last enum value
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
const component = (
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>
|
|
);
|
|
|
|
const { stdin, unmount, lastFrame } = render(component);
|
|
|
|
// Verify we're on Tool Approval Mode (first setting)
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('● Tool Approval Mode');
|
|
});
|
|
|
|
// Press Enter to cycle - should loop back to first value (Plan)
|
|
act(() => {
|
|
stdin.write(TerminalKeys.ENTER as string);
|
|
});
|
|
await wait();
|
|
await waitFor(() => {
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
|
});
|
|
|
|
// Should loop back to first enum value (Plan)
|
|
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
|
new Set<string>(['tools.approvalMode']),
|
|
expect.objectContaining({
|
|
tools: expect.objectContaining({
|
|
approvalMode: 'plan', // First enum value after YOLO
|
|
}),
|
|
}),
|
|
expect.any(LoadedSettings),
|
|
SettingScope.User,
|
|
);
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
it('should toggle setting with Space key', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Press Space to toggle current setting
|
|
stdin.write(' '); // Space key
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should handle vim mode setting specially', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Navigate to vim mode setting and toggle it
|
|
// This would require knowing the exact position, so we'll just test that the mock is called
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter key
|
|
await wait();
|
|
|
|
// The mock should potentially be called if vim mode was toggled
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Scope Selection', () => {
|
|
it('should switch between scopes', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Switch to scope focus
|
|
stdin.write(TerminalKeys.TAB); // Tab key
|
|
await wait();
|
|
|
|
// Select different scope (numbers 1-3 typically available)
|
|
stdin.write('2'); // Select second scope option
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should reset to settings focus when scope is selected', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Wait for initial render
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Tool Approval Mode');
|
|
});
|
|
|
|
// The UI should show settings mode is active (scope is in separate view)
|
|
expect(lastFrame()).toContain('● Tool Approval Mode'); // Settings section active
|
|
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
|
|
|
// This test validates the initial state - scope selection is now
|
|
// accessed via Tab key, not shown alongside settings
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Restart Prompt', () => {
|
|
it('should show restart prompt for restart-required settings', async () => {
|
|
const settings = createMockSettings();
|
|
const onRestartRequest = vi.fn();
|
|
|
|
const { unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog
|
|
settings={settings}
|
|
onSelect={() => {}}
|
|
onRestartRequest={onRestartRequest}
|
|
/>
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// This test would need to trigger a restart-required setting change
|
|
// The exact steps depend on which settings require restart
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should handle restart request when r is pressed', async () => {
|
|
const settings = createMockSettings();
|
|
const onRestartRequest = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog
|
|
settings={settings}
|
|
onSelect={() => {}}
|
|
onRestartRequest={onRestartRequest}
|
|
/>
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Press 'r' key (this would only work if restart prompt is showing)
|
|
stdin.write('r');
|
|
await wait();
|
|
|
|
// If restart prompt was showing, onRestartRequest should be called
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Escape Key Behavior', () => {
|
|
it('should call onSelect with undefined when Escape is pressed', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Wait for initial render
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Tool Approval Mode');
|
|
});
|
|
|
|
// Verify the dialog is rendered properly (scope is in separate view)
|
|
expect(lastFrame()).toContain('Settings');
|
|
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
|
|
|
// This test validates rendering - escape key behavior depends on complex
|
|
// keypress handling that's difficult to test reliably in this environment
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Settings Persistence', () => {
|
|
it('should persist settings across scope changes', async () => {
|
|
const settings = createMockSettings({ vimMode: true });
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Switch to scope selector
|
|
stdin.write(TerminalKeys.TAB as string); // Tab
|
|
await wait();
|
|
|
|
// Change scope
|
|
stdin.write('2'); // Select workspace scope
|
|
await wait();
|
|
|
|
// Settings should be reloaded for new scope
|
|
unmount();
|
|
});
|
|
|
|
it('should show different values for different scopes', () => {
|
|
const settings = createMockSettings(
|
|
{ vimMode: true }, // User settings
|
|
{ vimMode: false }, // System settings
|
|
{ autoUpdate: false }, // Workspace settings
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Should show user scope values initially
|
|
const output = lastFrame();
|
|
expect(output).toContain('Settings');
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle vim mode toggle errors gracefully', async () => {
|
|
mockToggleVimEnabled.mockRejectedValue(new Error('Toggle failed'));
|
|
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Try to toggle a setting (this might trigger vim mode toggle)
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// Should not crash
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Complex State Management', () => {
|
|
it('should track modified settings correctly', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Toggle a setting
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// Toggle another setting
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
|
await wait();
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// Should track multiple modified settings
|
|
unmount();
|
|
});
|
|
|
|
it('should handle scrolling when there are many settings', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Navigate down many times to test scrolling
|
|
for (let i = 0; i < 10; i++) {
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
|
await wait(10);
|
|
}
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('VimMode Integration', () => {
|
|
it('should sync with VimModeContext when vim mode is toggled', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<VimModeProvider settings={settings}>
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>
|
|
</VimModeProvider>,
|
|
);
|
|
|
|
// Navigate to and toggle vim mode setting
|
|
// This would require knowing the exact position of vim mode setting
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Specific Settings Behavior', () => {
|
|
it('should show correct display values for settings with different states', () => {
|
|
const settings = createMockSettings(
|
|
{ vimMode: true, hideTips: false }, // User settings
|
|
{ hideWindowTitle: true }, // System settings
|
|
{ ideMode: false }, // Workspace settings
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// Should contain settings labels
|
|
expect(output).toContain('Settings');
|
|
});
|
|
|
|
it('should handle immediate settings save for non-restart-required settings', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Toggle a non-restart-required setting (like hideTips)
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter - toggle current setting
|
|
await wait();
|
|
|
|
// Should save immediately without showing restart prompt
|
|
unmount();
|
|
});
|
|
|
|
it('should show restart prompt for restart-required settings', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// This test would need to navigate to a specific restart-required setting
|
|
// Since we can't easily target specific settings, we test the general behavior
|
|
await wait();
|
|
|
|
// Should not show restart prompt initially
|
|
expect(lastFrame()).not.toContain(
|
|
'To see changes, Qwen Code must be restarted',
|
|
);
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should keep restart prompt when switching scopes', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Tool Approval Mode');
|
|
});
|
|
|
|
const languageIndex = getDialogSettingKeys().indexOf('general.language');
|
|
expect(languageIndex).toBeGreaterThanOrEqual(0);
|
|
|
|
const press = async (key: string) => {
|
|
act(() => {
|
|
stdin.write(key);
|
|
});
|
|
await wait();
|
|
};
|
|
|
|
// Trigger a restart-required setting change by toggling the UI language setting.
|
|
for (let i = 0; i < languageIndex; i++) {
|
|
await press(TerminalKeys.DOWN_ARROW as string);
|
|
}
|
|
await press(TerminalKeys.ENTER as string);
|
|
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain(
|
|
'To see changes, Qwen Code must be restarted',
|
|
);
|
|
});
|
|
|
|
// Switch scopes; restart prompt should remain visible.
|
|
await press(TerminalKeys.TAB as string);
|
|
await press('2');
|
|
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain(
|
|
'To see changes, Qwen Code must be restarted',
|
|
);
|
|
});
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Settings Display Values', () => {
|
|
it('should show correct values for inherited settings', () => {
|
|
const settings = createMockSettings(
|
|
{},
|
|
{ vimMode: true, hideWindowTitle: false }, // System settings
|
|
{},
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// Settings should show inherited values
|
|
expect(output).toContain('Settings');
|
|
});
|
|
|
|
it('should show override indicator for overridden settings', () => {
|
|
const settings = createMockSettings(
|
|
{ vimMode: false }, // User overrides
|
|
{ vimMode: true }, // System default
|
|
{},
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
// Should show settings with override indicators
|
|
expect(output).toContain('Settings');
|
|
});
|
|
});
|
|
|
|
describe('Output Language', () => {
|
|
it('treats empty output language as auto', async () => {
|
|
const settings = createMockSettings({
|
|
general: { outputLanguage: 'en' },
|
|
});
|
|
|
|
const { stdin, unmount, lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={() => {}} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Settings');
|
|
});
|
|
|
|
// Navigate to the output language setting, start editing, then commit empty.
|
|
// Avoid hard-coding the item index because schema-driven ordering can differ by platform.
|
|
const outputLanguageIndex = getDialogSettingKeys().indexOf(
|
|
'general.outputLanguage',
|
|
);
|
|
expect(outputLanguageIndex).toBeGreaterThanOrEqual(0);
|
|
|
|
const press = async (key: string) => {
|
|
act(() => {
|
|
stdin.write(key);
|
|
});
|
|
await wait();
|
|
};
|
|
|
|
for (let i = 0; i < outputLanguageIndex; i++) {
|
|
await press(TerminalKeys.DOWN_ARROW as string);
|
|
}
|
|
await press(TerminalKeys.ENTER as string);
|
|
await press(TerminalKeys.ENTER as string);
|
|
|
|
// Empty input should set 'auto' in settings (rule file is updated on restart)
|
|
await waitFor(() => {
|
|
const outputLanguageCall = vi
|
|
.mocked(saveModifiedSettings)
|
|
.mock.calls.find((call) =>
|
|
(call[0] as Set<string>).has('general.outputLanguage'),
|
|
);
|
|
expect(outputLanguageCall).toBeTruthy();
|
|
});
|
|
|
|
const outputLanguageCall = vi
|
|
.mocked(saveModifiedSettings)
|
|
.mock.calls.find((call) =>
|
|
(call[0] as Set<string>).has('general.outputLanguage'),
|
|
);
|
|
// Should save 'auto' to settings
|
|
expect(outputLanguageCall?.[1]).toMatchObject({
|
|
general: { outputLanguage: OUTPUT_LANGUAGE_AUTO },
|
|
});
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Keyboard Shortcuts Edge Cases', () => {
|
|
it('should handle rapid key presses gracefully', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Rapid navigation
|
|
for (let i = 0; i < 5; i++) {
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
|
stdin.write(TerminalKeys.UP_ARROW as string);
|
|
}
|
|
await wait(100);
|
|
|
|
// Should not crash
|
|
unmount();
|
|
});
|
|
|
|
it('should handle Ctrl+C to reset current setting to default', async () => {
|
|
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Press Ctrl+C to reset current setting to default
|
|
stdin.write('\u0003'); // Ctrl+C
|
|
await wait();
|
|
|
|
// Should reset the current setting to its default value
|
|
unmount();
|
|
});
|
|
|
|
it('should handle Ctrl+L to reset current setting to default', async () => {
|
|
const settings = createMockSettings({ vimMode: true }); // Start with vimMode enabled
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Press Ctrl+L to reset current setting to default
|
|
stdin.write('\u000C'); // Ctrl+L
|
|
await wait();
|
|
|
|
// Should reset the current setting to its default value
|
|
unmount();
|
|
});
|
|
|
|
it('should handle navigation when only one setting exists', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Try to navigate when potentially at bounds
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
|
await wait();
|
|
stdin.write(TerminalKeys.UP_ARROW as string);
|
|
await wait();
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should properly handle Tab navigation between sections', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Wait for initial render
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Tool Approval Mode');
|
|
});
|
|
|
|
// Verify initial state: settings mode active (scope is in separate view)
|
|
expect(lastFrame()).toContain('● Tool Approval Mode'); // Settings mode active
|
|
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
|
|
|
// This test validates the rendered UI structure for tab navigation
|
|
// Tab now switches between settings view and scope view
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Error Recovery', () => {
|
|
it('should handle malformed settings gracefully', () => {
|
|
// Create settings with potentially problematic values
|
|
const settings = createMockSettings(
|
|
{ vimMode: null as unknown as boolean }, // Invalid value
|
|
{},
|
|
{},
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Should still render without crashing
|
|
expect(lastFrame()).toContain('Settings');
|
|
});
|
|
|
|
it('should handle missing setting definitions gracefully', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
// Should not crash even if some settings are missing definitions
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toContain('Settings');
|
|
});
|
|
});
|
|
|
|
describe('Complex User Interactions', () => {
|
|
it('should handle complete user workflow: navigate, toggle, change scope, exit', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Wait for initial render
|
|
await waitFor(() => {
|
|
expect(lastFrame()).toContain('Tool Approval Mode');
|
|
});
|
|
|
|
// Verify the complete UI is rendered (scope is in separate view)
|
|
expect(lastFrame()).toContain('Settings'); // Title
|
|
expect(lastFrame()).toContain('● Tool Approval Mode'); // Active setting
|
|
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view (Tab to access)
|
|
expect(lastFrame()).toContain(
|
|
'(Use Enter to select, Tab to configure scope)',
|
|
); // Help text
|
|
|
|
// This test validates the complete UI structure is available for user workflow
|
|
// Scope selection is now accessed via Tab key (view switching like ThemeDialog)
|
|
|
|
unmount();
|
|
});
|
|
|
|
it('should allow changing multiple settings without losing pending changes', async () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Toggle first setting (should require restart)
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// Navigate to next setting and toggle it (should not require restart - e.g., vimMode)
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
|
await wait();
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// Navigate to another setting and toggle it (should also require restart)
|
|
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
|
await wait();
|
|
stdin.write(TerminalKeys.ENTER as string); // Enter
|
|
await wait();
|
|
|
|
// The test verifies that all changes are preserved and the dialog still works
|
|
// This tests the fix for the bug where changing one setting would reset all pending changes
|
|
unmount();
|
|
});
|
|
|
|
it('should maintain state consistency during complex interactions', async () => {
|
|
const settings = createMockSettings({ vimMode: true });
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Multiple scope changes
|
|
stdin.write(TerminalKeys.TAB as string); // Tab to scope
|
|
await wait();
|
|
stdin.write('2'); // Workspace
|
|
await wait();
|
|
stdin.write(TerminalKeys.TAB as string); // Tab to settings
|
|
await wait();
|
|
stdin.write(TerminalKeys.TAB as string); // Tab to scope
|
|
await wait();
|
|
stdin.write('1'); // User
|
|
await wait();
|
|
|
|
// Should maintain consistent state
|
|
unmount();
|
|
});
|
|
|
|
it('should handle restart workflow correctly', async () => {
|
|
const settings = createMockSettings();
|
|
const onRestartRequest = vi.fn();
|
|
|
|
const { stdin, unmount } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog
|
|
settings={settings}
|
|
onSelect={() => {}}
|
|
onRestartRequest={onRestartRequest}
|
|
/>
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// This would test the restart workflow if we could trigger it
|
|
stdin.write('r'); // Try restart key
|
|
await wait();
|
|
|
|
// Without restart prompt showing, this should have no effect
|
|
expect(onRestartRequest).not.toHaveBeenCalled();
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('String Settings Editing', () => {
|
|
it('should allow editing and committing a string setting', async () => {
|
|
let settings = createMockSettings({ 'a.string.setting': 'initial' });
|
|
const onSelect = vi.fn();
|
|
|
|
const { stdin, unmount, rerender } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Wait for the dialog to render
|
|
await wait();
|
|
|
|
// Navigate to the last setting
|
|
for (let i = 0; i < 20; i++) {
|
|
stdin.write('j'); // Down
|
|
await wait(10);
|
|
}
|
|
|
|
// Press Enter to start editing
|
|
stdin.write('\r');
|
|
await wait();
|
|
|
|
// Type a new value
|
|
stdin.write('new value');
|
|
await wait();
|
|
|
|
// Press Enter to commit
|
|
stdin.write('\r');
|
|
await wait();
|
|
|
|
settings = createMockSettings(
|
|
{ 'a.string.setting': 'new value' },
|
|
{},
|
|
{},
|
|
);
|
|
rerender(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
await wait();
|
|
|
|
// Press Escape to exit
|
|
stdin.write('\u001B');
|
|
await wait();
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
|
|
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('Snapshot Tests', () => {
|
|
/**
|
|
* Snapshot tests for SettingsDialog component using ink-testing-library.
|
|
* These tests capture the visual output of the component in various states:
|
|
*
|
|
* - Default rendering with no custom settings
|
|
* - Various combinations of boolean settings (enabled/disabled)
|
|
* - Mixed boolean and number settings configurations
|
|
* - Different focus states (settings vs scope selector)
|
|
* - Different scope selections (User, System, Workspace)
|
|
* - Accessibility settings enabled
|
|
* - File filtering configurations
|
|
* - Tools and security settings
|
|
* - All settings disabled state
|
|
*
|
|
* The snapshots help ensure UI consistency and catch unintended visual changes.
|
|
*/
|
|
|
|
it('should render default state correctly', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with various boolean settings enabled', () => {
|
|
const settings = createMockSettings({
|
|
general: {
|
|
vimMode: true,
|
|
disableAutoUpdate: true,
|
|
debugKeystrokeLogging: true,
|
|
},
|
|
ui: {
|
|
hideWindowTitle: true,
|
|
hideTips: true,
|
|
showLineNumbers: true,
|
|
showCitations: true,
|
|
accessibility: {
|
|
disableLoadingPhrases: true,
|
|
screenReader: true,
|
|
},
|
|
},
|
|
ide: {
|
|
enabled: true,
|
|
},
|
|
context: {
|
|
loadFromIncludeDirectories: true,
|
|
fileFiltering: {
|
|
respectGitIgnore: true,
|
|
respectQwenIgnore: true,
|
|
enableRecursiveFileSearch: true,
|
|
disableFuzzySearch: false,
|
|
},
|
|
},
|
|
tools: {
|
|
enableInteractiveShell: true,
|
|
autoAccept: true,
|
|
useRipgrep: true,
|
|
},
|
|
security: {
|
|
folderTrust: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with mixed boolean and number settings', () => {
|
|
const settings = createMockSettings({
|
|
general: {
|
|
vimMode: false,
|
|
disableAutoUpdate: true,
|
|
},
|
|
ui: {
|
|
hideWindowTitle: false,
|
|
},
|
|
tools: {
|
|
truncateToolOutputThreshold: 50000,
|
|
truncateToolOutputLines: 1000,
|
|
},
|
|
context: {},
|
|
model: {
|
|
maxSessionTurns: 100,
|
|
skipNextSpeakerCheck: false,
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render focused on scope selector', () => {
|
|
const settings = createMockSettings();
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, stdin } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Switch focus to scope selector with Tab
|
|
stdin.write('\t');
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with different scope selected (System)', () => {
|
|
const settings = createMockSettings(
|
|
{}, // userSettings
|
|
{
|
|
// systemSettings
|
|
general: {
|
|
vimMode: true,
|
|
disableAutoUpdate: false,
|
|
},
|
|
ui: {},
|
|
},
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, stdin } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Switch to scope selector
|
|
stdin.write('\t');
|
|
// Navigate to System scope
|
|
stdin.write('ArrowDown');
|
|
stdin.write('\r'); // Enter to select
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with different scope selected (Workspace)', () => {
|
|
const settings = createMockSettings(
|
|
{}, // userSettings
|
|
{}, // systemSettings
|
|
{
|
|
// workspaceSettings
|
|
general: {
|
|
vimMode: false,
|
|
debugKeystrokeLogging: true,
|
|
},
|
|
tools: {
|
|
useRipgrep: true,
|
|
enableInteractiveShell: false,
|
|
},
|
|
},
|
|
);
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame, stdin } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
// Switch to scope selector
|
|
stdin.write('\t');
|
|
// Navigate to Workspace scope (down twice)
|
|
stdin.write('ArrowDown');
|
|
stdin.write('ArrowDown');
|
|
stdin.write('\r'); // Enter to select
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with accessibility settings enabled', () => {
|
|
const settings = createMockSettings({
|
|
ui: {
|
|
accessibility: {
|
|
disableLoadingPhrases: true,
|
|
screenReader: true,
|
|
},
|
|
showLineNumbers: true,
|
|
},
|
|
general: {
|
|
vimMode: true,
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with file filtering settings configured', () => {
|
|
const settings = createMockSettings({
|
|
context: {
|
|
fileFiltering: {
|
|
respectGitIgnore: false,
|
|
respectQwenIgnore: true,
|
|
enableRecursiveFileSearch: false,
|
|
disableFuzzySearch: true,
|
|
},
|
|
loadFromIncludeDirectories: true,
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with tools and security settings', () => {
|
|
const settings = createMockSettings({
|
|
tools: {
|
|
enableInteractiveShell: true,
|
|
autoAccept: false,
|
|
useRipgrep: true,
|
|
truncateToolOutputThreshold: 25000,
|
|
truncateToolOutputLines: 500,
|
|
},
|
|
security: {
|
|
folderTrust: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
model: {
|
|
maxSessionTurns: 50,
|
|
skipNextSpeakerCheck: true,
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render with all boolean settings disabled', () => {
|
|
const settings = createMockSettings({
|
|
general: {
|
|
vimMode: false,
|
|
disableAutoUpdate: false,
|
|
debugKeystrokeLogging: false,
|
|
},
|
|
ui: {
|
|
hideWindowTitle: false,
|
|
hideTips: false,
|
|
showLineNumbers: false,
|
|
showCitations: false,
|
|
accessibility: {
|
|
disableLoadingPhrases: false,
|
|
screenReader: false,
|
|
},
|
|
},
|
|
ide: {
|
|
enabled: false,
|
|
},
|
|
context: {
|
|
loadFromIncludeDirectories: false,
|
|
fileFiltering: {
|
|
respectGitIgnore: false,
|
|
respectQwenIgnore: false,
|
|
enableRecursiveFileSearch: false,
|
|
disableFuzzySearch: false,
|
|
},
|
|
},
|
|
tools: {
|
|
enableInteractiveShell: false,
|
|
autoAccept: false,
|
|
useRipgrep: false,
|
|
},
|
|
security: {
|
|
folderTrust: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
});
|
|
const onSelect = vi.fn();
|
|
|
|
const { lastFrame } = render(
|
|
<KeypressProvider kittyProtocolEnabled={false}>
|
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
|
</KeypressProvider>,
|
|
);
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
});
|
|
});
|
|
});
|