3616d35a75
Migrates the 8 try/except sites in UI + theme + tooling files
by narrowing the exception types from broad 'except Exception' to
specific stdlib/domain exceptions.
Files and sites:
1. src/command_palette.py:120 (1 site) - command.action callback
except Exception -> except (AttributeError, TypeError, ValueError, OSError)
2. src/commands.py:116 (1 site) - generate_md
except Exception -> except (OSError, ValueError, TypeError)
3. src/commands.py:147 (1 site) - save_all
except Exception -> except (OSError, ValueError)
4. src/commands.py:271 (1 site) - reset_layout
except Exception -> except OSError
5. src/diff_viewer.py:167 (1 site) - apply_patch
except Exception -> except (OSError, ValueError, IndexError)
6. src/external_editor.py:82 (1 site) - powershell reg lookup
except Exception -> except (OSError, subprocess.SubprocessError,
subprocess.TimeoutExpired)
7. src/markdown_helper.py:123 (1 site) - open link
except Exception -> except (OSError, ValueError)
8. src/markdown_helper.py:200 (1 site) - render_table fallback
except Exception -> except (TypeError, AttributeError, ValueError, IndexError)
Also updates tests/test_command_palette_sim.py to use TypeError
(caught by the narrowing) instead of RuntimeError (not caught).
Decisions:
- theme_2.py:282 already narrow (ImportError, AttributeError); no change
- theme_models.py:166 is RAISE (not except); keep as-is (documented)
- external_editor.py:47, 56 already narrow (FileNotFoundError); no change
Tests verified:
- tests/test_command_palette.py (13 tests) PASS
- tests/test_command_palette_sim.py (7 tests) PASS
- tests/test_diff_viewer.py (10 tests) PASS
- tests/test_external_editor.py (16 tests) PASS
- tests/test_external_editor_gui.py (5 tests) PASS
- tests/test_markdown_helper_* (16 tests) PASS
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""Live GUI tests for the Command Palette feature.
|
|
|
|
Uses the live_gui fixture and the ApiHookClient to:
|
|
1. Toggle the palette via the custom_callback (since the Ctrl+Shift+P
|
|
keyboard shortcut cannot be simulated through the hook API).
|
|
2. Verify the palette state is queryable via the gettable field.
|
|
3. Confirm the registered commands are visible to the system.
|
|
"""
|
|
from __future__ import annotations
|
|
import time
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from src.api_hook_client import ApiHookClient
|
|
from src.commands import registry
|
|
|
|
|
|
def test_palette_starts_hidden(live_gui: Any) -> None:
|
|
"""On startup, the palette should be closed."""
|
|
client = ApiHookClient()
|
|
state = client.get_value("show_command_palette")
|
|
assert state is not None, "show_command_palette should be a gettable field"
|
|
assert state is False, f"Palette should start hidden, got {state}"
|
|
|
|
|
|
def test_palette_toggles_via_callback(live_gui: Any) -> None:
|
|
"""The _toggle_command_palette callback should open and close the palette."""
|
|
client = ApiHookClient()
|
|
assert client.get_value("show_command_palette") is False
|
|
|
|
# Open via custom callback
|
|
client.push_event("custom_callback", {
|
|
"callback": "_toggle_command_palette",
|
|
"args": [],
|
|
})
|
|
time.sleep(0.5)
|
|
assert client.get_value("show_command_palette") is True, "Palette should be open after toggle"
|
|
|
|
# Close via custom callback
|
|
client.push_event("custom_callback", {
|
|
"callback": "_toggle_command_palette",
|
|
"args": [],
|
|
})
|
|
time.sleep(0.5)
|
|
assert client.get_value("show_command_palette") is False, "Palette should be closed after second toggle"
|
|
|
|
|
|
def test_palette_registers_core_commands(live_gui: Any) -> None:
|
|
"""Verify that the core commands are registered and have actions.
|
|
|
|
The palette modal calls registry.all() when rendering. If the registry
|
|
is empty or commands lack actions, the palette will show 'No matching
|
|
commands' for any query.
|
|
"""
|
|
all_commands = registry.all()
|
|
ids = {c.id for c in all_commands}
|
|
assert "reset_session" in ids
|
|
assert "clear_discussion" in ids
|
|
assert "trigger_hot_reload" in ids
|
|
assert "show_documentation" in ids
|
|
|
|
# Every command must have a callable action
|
|
for cmd in all_commands:
|
|
assert cmd.action is not None
|
|
assert callable(cmd.action)
|
|
|
|
|
|
def test_palette_query_state_resets_on_open(live_gui: Any) -> None:
|
|
"""Opening the palette resets _command_palette_query and _command_palette_selected.
|
|
|
|
This ensures a fresh state every time the user opens the palette.
|
|
"""
|
|
client = ApiHookClient()
|
|
|
|
# Open once, set some state (simulate user typing)
|
|
client.push_event("custom_callback", {
|
|
"callback": "_toggle_command_palette",
|
|
"args": [],
|
|
})
|
|
time.sleep(0.5)
|
|
|
|
# Close
|
|
client.push_event("custom_callback", {
|
|
"callback": "_toggle_command_palette",
|
|
"args": [],
|
|
})
|
|
time.sleep(0.5)
|
|
|
|
# Reopen — state should be reset (query is back to empty string)
|
|
client.push_event("custom_callback", {
|
|
"callback": "_toggle_command_palette",
|
|
"args": [],
|
|
})
|
|
time.sleep(0.5)
|
|
|
|
assert client.get_value("show_command_palette") is True
|
|
# The internal _command_palette_query is set to "" on open.
|
|
# We can't directly query it via the hook, but the state being
|
|
# queryable via show_command_palette confirms the toggle worked.
|
|
|
|
|
|
def test_palette_close_helper_resets_all_state() -> None:
|
|
"""_close_palette resets all per-open state flags. Pure unit test."""
|
|
from src.command_palette import _close_palette
|
|
from unittest.mock import MagicMock
|
|
|
|
mock_app = MagicMock()
|
|
mock_app.show_command_palette = True
|
|
mock_app._command_palette_query = "save"
|
|
mock_app._command_palette_selected = 2
|
|
mock_app._command_palette_focused = True
|
|
mock_app._command_palette_input_focused = True
|
|
|
|
_close_palette(mock_app)
|
|
|
|
assert mock_app.show_command_palette is False
|
|
assert mock_app._command_palette_query == ""
|
|
assert mock_app._command_palette_selected == 0
|
|
assert mock_app._command_palette_focused is False
|
|
assert mock_app._command_palette_input_focused is False
|
|
|
|
|
|
def test_execute_runs_command_and_closes() -> None:
|
|
"""_execute runs a command and closes the palette, catching exceptions. Pure unit test."""
|
|
from src.command_palette import _execute, Command
|
|
from unittest.mock import MagicMock
|
|
|
|
# Build a mock app and a "good" command
|
|
mock_app = MagicMock()
|
|
mock_app.show_command_palette = True
|
|
good_command = Command(
|
|
id="test_good",
|
|
title="Test Good",
|
|
category="test",
|
|
action=lambda app: setattr(app, "ran", True),
|
|
)
|
|
_execute(mock_app, good_command)
|
|
assert mock_app.ran is True
|
|
assert mock_app.show_command_palette is False
|
|
|
|
# Build a "bad" command that raises
|
|
bad_app = MagicMock()
|
|
bad_app.show_command_palette = True
|
|
bad_command = Command(
|
|
id="test_bad",
|
|
title="Test Bad",
|
|
category="test",
|
|
action=lambda app: (_ for _ in ()).throw(TypeError("boom")),
|
|
)
|
|
# Should NOT raise
|
|
_execute(bad_app, bad_command)
|
|
# Palette should still close
|
|
assert bad_app.show_command_palette is False
|
|
|
|
|
|
def test_fuzzy_match_returns_top_n_for_navigation() -> None:
|
|
"""The palette returns up to top_n results so Up/Down navigation is meaningful."""
|
|
from src.commands import registry
|
|
all_commands = registry.all()
|
|
# We have 11 commands; fuzzy_match with empty query returns all
|
|
# Verify there are enough commands to navigate through with Up/Down
|
|
assert len(all_commands) >= 3, "Need at least 3 commands to test navigation"
|