3dd153f718
Per spec FR1 + Phase 1.3 + architecture feedback: src/command_palette.py
split by responsibility:
- Command/ScoredCommand/CommandRegistry/fuzzy_match/_close_palette/_execute (data/ops)
-> src/commands.py (which already owns _LazyCommandRegistry pattern)
- render_palette_modal (view/ImGui) -> src/gui_2.py
GUI is a pure view; the registry/data classes are ops; commands.py owns
the registry because commands.py is where @registry.register decorators live.
gui_2.render_palette_modal imports Command from commands.py to type its
parameters.
Also fixes Phase 1.1 (bg_shader) per architecture feedback:
BackgroundShader no longer owns 'enabled' state - the GUI is pure view.
State is now owned by AppController.bg_shader_enabled (read on load from
config, written from gui_2 checkbox via app's __setattr__ delegation).
Tests:
- tests/test_command_palette.py: imports from src.commands (was src.command_palette)
- tests/test_commands_no_top_level_command_palette.py: rewritten for the
new architecture (eager registry in commands.py; render in gui_2; no
circular import between commands.py and gui_2)
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Tests for the post-Phase 1.3 architecture.
|
|
|
|
Per module_taxonomy_refactor_20260627 Phase 1.3, src/command_palette.py was
|
|
deleted and its content split by responsibility:
|
|
- Command / ScoredCommand / CommandRegistry / fuzzy_match (data/ops) -> src/commands.py
|
|
- render_palette_modal (view) -> src/gui_2.py
|
|
|
|
src/commands.py is a thin data module and can be imported eagerly (no
|
|
require_warmed lazy load is needed; the original lazy-load pattern in
|
|
startup_speedup_20260606 was specifically to defer the heavy src/command_palette
|
|
which pulled in imgui at module load).
|
|
|
|
These tests run in fresh subprocesses to ensure no warmup state leaks from
|
|
the test runner. We assert:
|
|
- src/commands imports cleanly and exposes Command + CommandRegistry
|
|
- src/gui_2 exposes render_palette_modal
|
|
- src/commands does NOT import gui_2 at module level (avoids circular)
|
|
- The static audit detects no top-level command_palette import (since the
|
|
module no longer exists)
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _run_in_subprocess(snippet: str) -> subprocess.CompletedProcess:
|
|
script = textwrap.dedent(snippet)
|
|
return subprocess.run(
|
|
[sys.executable, "-c", script],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(ROOT),
|
|
timeout=30,
|
|
)
|
|
|
|
|
|
def test_commands_exposes_command_and_registry() -> None:
|
|
res = _run_in_subprocess("""
|
|
from src.commands import Command, CommandRegistry, fuzzy_match, ScoredCommand
|
|
r = CommandRegistry()
|
|
def my_cmd(app): pass
|
|
r.register(my_cmd)
|
|
print(len(r.all()))
|
|
print(r.all()[0].id)
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
lines = res.stdout.strip().splitlines()
|
|
assert lines[0] == "1"
|
|
assert lines[1] == "my_cmd"
|
|
|
|
|
|
def test_gui_2_exposes_render_palette_modal() -> None:
|
|
res = _run_in_subprocess("""
|
|
from src.gui_2 import render_palette_modal
|
|
print(callable(render_palette_modal))
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert res.stdout.strip() == "True"
|
|
|
|
|
|
def test_commands_does_not_import_gui_2_at_module_level() -> None:
|
|
"""src/commands is imported by src/commands registration sites and by
|
|
gui_2.render_palette_modal. To avoid a circular import, commands.py must
|
|
NOT import gui_2 at the top of the file. (TYPE_CHECKING imports are
|
|
allowed because they don't execute at runtime.)
|
|
"""
|
|
res = _run_in_subprocess("""
|
|
import ast
|
|
from pathlib import Path
|
|
commands_path = Path('src') / 'commands.py'
|
|
source = commands_path.read_text(encoding='utf-8')
|
|
tree = ast.parse(source)
|
|
for node in tree.body:
|
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
mod = getattr(node, 'module', None) or (node.names[0].name if node.names else '')
|
|
if mod and ('gui_2' in mod or mod.endswith('gui_2')):
|
|
print('VIOLATION:', mod)
|
|
print('OK')
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert "VIOLATION" not in res.stdout
|
|
assert "OK" in res.stdout
|
|
|
|
|
|
def test_command_palette_module_no_longer_exists() -> None:
|
|
"""src/command_palette.py was deleted in Phase 1.3; this is a regression guard."""
|
|
res = _run_in_subprocess("""
|
|
import importlib
|
|
try:
|
|
importlib.import_module('src.command_palette')
|
|
print('EXISTS')
|
|
except ModuleNotFoundError:
|
|
print('NOT_FOUND')
|
|
""")
|
|
assert res.returncode == 0, f"stderr: {res.stderr}"
|
|
assert res.stdout.strip() == "NOT_FOUND" |