diff --git a/.gitignore b/.gitignore index e8ebf0e6..b6028a5f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ sdm_report_refined.json session-ses_1eb8.md mock_debug_prompt.txt temp_old_gui.py +.slop_cache/summary_cache.json diff --git a/scripts/py_struct_tools.py b/scripts/py_struct_tools.py index 5e8475c2..fcb10f2e 100644 --- a/scripts/py_struct_tools.py +++ b/scripts/py_struct_tools.py @@ -11,7 +11,7 @@ def find_definition_range(source: str, symbol_path: str) -> tuple[int, int] | No try: tree = ast.parse(source) except SyntaxError: - return None + return _find_definition_range_regex(source, symbol_path) parts = symbol_path.split('.') current_nodes = tree.body target_node = None @@ -21,13 +21,11 @@ def find_definition_range(source: str, symbol_path: str) -> tuple[int, int] | No if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name == part: if i == len(parts) - 1: target_node = node - found = True - break else: if isinstance(node, ast.ClassDef): current_nodes = node.body - found = True - break + found = True + break if not found: return None if target_node: @@ -37,6 +35,42 @@ def find_definition_range(source: str, symbol_path: str) -> tuple[int, int] | No return (start, target_node.end_lineno) return None +def _find_definition_range_regex(source: str, symbol_path: str) -> tuple[int, int] | None: + import re + parts = symbol_path.split('.') + symbol_name = parts[-1] + if '.' in symbol_path: + class_name = parts[0] + class_pattern = r'class\s+' + re.escape(class_name) + r'\s*:' + class_match = re.search(class_pattern, source) + if not class_match: + return None + class_body_start = class_match.end() + method_pattern = r'def\s+' + re.escape(symbol_name) + r'\s*\(' + method_match = re.search(method_pattern, source[class_body_start:]) + if not method_match: + return None + method_line_num = source[:class_body_start + method_match.start()].count('\n') + 1 + remaining = source[class_body_start + method_match.start():] + end_pattern = r'\n(?= def\s|\nclass\s|\Z)' + end_match = re.search(end_pattern, remaining) + if end_match: + end_line = source[:class_body_start + method_match.start() + end_match.start()].count('\n') + 1 + return (method_line_num, end_line) + return None + else: + func_pattern = r'def\s+' + re.escape(symbol_name) + r'\s*\(' + func_match = re.search(func_pattern, source) + if not func_match: + return None + start = source[:func_match.start()].count('\n') + 1 + remaining = source[func_match.start():] + end_match = re.search(r'\n(?=def\s|\Z)', remaining) + if end_match: + end = remaining[:end_match.start()].count('\n') + start + return (start, end) + return None + def shift_indentation(content: str, target_depth: int) -> str: """ Shifts and normalizes the indentation of a code block to 1-space units. diff --git a/tests/test_gui_kill_button.py b/tests/test_gui_kill_button.py index 82edf29e..deedf126 100644 --- a/tests/test_gui_kill_button.py +++ b/tests/test_gui_kill_button.py @@ -26,7 +26,6 @@ def test_render_ticket_queue_table_columns(): mock_imgui.pop_style_color = MagicMock() mock_imgui.same_line = MagicMock() - # Setup imscope mocks mock_imscope.window.return_value.__enter__.return_value = (True, True) mock_imscope.child.return_value.__enter__.return_value = True mock_imscope.table.return_value.__enter__.return_value = True @@ -34,8 +33,7 @@ def test_render_ticket_queue_table_columns(): mock_imscope.tab_item.return_value.__enter__.return_value = (True, True) mock_imscope.style_color.return_value.__enter__.return_value = None mock_imscope.style_var.return_value.__enter__.return_value = None - - from src.gui_2 import App + from src.gui_2 import App, render_ticket_queue app = App.__new__(App) app.active_track = MagicMock() app.active_tickets = [{"id": "T-001", "priority": "medium", "status": "in_progress", "description": "Test task"}] @@ -44,6 +42,6 @@ def test_render_ticket_queue_table_columns(): app.controller = MagicMock() app._push_mma_state_update = MagicMock() app._cb_kill_ticket = MagicMock() - app._render_ticket_queue() + render_ticket_queue(app) columns_called = [call[0][0] for call in mock_imgui.table_setup_column.call_args_list] - assert "Actions" in columns_called, f"Expected Actions column, got: {columns_called}" + assert "Actions" in columns_called, f"Expected Actions column, got: {columns_called}" \ No newline at end of file diff --git a/tests/test_project_settings_rename.py b/tests/test_project_settings_rename.py index 4e8e4e61..54326faf 100644 --- a/tests/test_project_settings_rename.py +++ b/tests/test_project_settings_rename.py @@ -5,7 +5,7 @@ import inspect def test_context_hub_renamed_to_project_settings(): import src.gui_2 as gui_2 - source = inspect.getsource(gui_2.App._render_main_interface) + source = inspect.getsource(gui_2.render_main_interface) assert "Project Settings" in source, ( "Context Hub should be renamed to Project Settings" ) diff --git a/tests/test_py_struct_tools.py b/tests/test_py_struct_tools.py index a8676980..2b44c7d7 100644 --- a/tests/test_py_struct_tools.py +++ b/tests/test_py_struct_tools.py @@ -5,140 +5,140 @@ from src import mcp_client @pytest.fixture def temp_py_file(tmp_path): - p = tmp_path / "sample.py" - content = """class MyClass: -\"\"\"Docstring.\"\"\" -def method1(self): -print("m1") + p = tmp_path / "sample.py" + content = """class MyClass: + \"\"\"Docstring.\"\"\" + def method1(self): + print("m1") def top_func(): -\"\"\"Top doc.\"\"\" -print("top") + \"\"\"Top doc.\"\"\" + print("top") """ - p.write_text(content, encoding="utf-8") - return str(p) + p.write_text(content, encoding="utf-8") + return str(p) def test_find_definition_range(): - source = """class A: -def m(self): pass + source = """class A: + def m(self): pass def f(): pass """ - assert py_struct_tools.find_definition_range(source, "A") == (1, 2) - assert py_struct_tools.find_definition_range(source, "A.m") == (2, 2) - assert py_struct_tools.find_definition_range(source, "f") == (3, 3) - assert py_struct_tools.find_definition_range(source, "nonexistent") is None + assert py_struct_tools.find_definition_range(source, "A") == (1, 2) + assert py_struct_tools.find_definition_range(source, "A.m") == (2, 2) + assert py_struct_tools.find_definition_range(source, "f") == (3, 3) + assert py_struct_tools.find_definition_range(source, "nonexistent") is None def test_shift_indentation(): - payload = "def f():\n print('hi')" # 2-space - shifted = py_struct_tools.shift_indentation(payload, 1) - assert shifted == " def f():\n print('hi')" # wait, shift_indentation strips min and prepends. + payload = "def f():\n print('hi')" # 2-space + shifted = py_struct_tools.shift_indentation(payload, 1) + assert shifted == " def f():\n print('hi')" # wait, shift_indentation strips min and prepends. - # Let's re-test shift_indentation logic - # Original: - # line 1: 'def f():' (0 indent) - # line 2: ' print('hi')' (2 indent) - # min_indent = 0 - # Prepend 1 space: - # ' def f():' - # ' print('hi')' + # Let's re-test shift_indentation logic + # Original: + # line 1: 'def f():' (0 indent) + # line 2: ' print('hi')' (2 indent) + # min_indent = 0 + # Prepend 1 space: + # ' def f():' + # ' print('hi')' - # If payload was: - # def f(): - # print('hi') - # min_indent = 2 - # target_depth = 1 - # ' def f():' - # ' print('hi')' + # If payload was: + # def f(): + # print('hi') + # min_indent = 2 + # target_depth = 1 + # ' def f():' + # ' print('hi')' - payload2 = " def f():\n print('hi')" - shifted2 = py_struct_tools.shift_indentation(payload2, 1) - assert shifted2 == " def f():\n print('hi')" + payload2 = " def f():\n print('hi')" + shifted2 = py_struct_tools.shift_indentation(payload2, 1) + assert shifted2 == " def f():\n print('hi')" def test_py_remove_def(temp_py_file): - err = py_struct_tools.py_remove_def(temp_py_file, "MyClass.method1") - assert err == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert "def method1" not in content - assert "class MyClass" in content + err = py_struct_tools.py_remove_def(temp_py_file, "MyClass.method1") + assert err == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert "def method1" not in content + assert "class MyClass" in content def test_py_add_def(temp_py_file): - new_code = "def method2(self):\n print('m2')" - err = py_struct_tools.py_add_def(temp_py_file, "MyClass", new_code, "after", "method1") - assert err == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert "def method2" in content - # Check 1-space indentation - assert " def method2(self):" in content + new_code = "def method2(self):\n print('m2')" + err = py_struct_tools.py_add_def(temp_py_file, "MyClass", new_code, "after", "method1") + assert err == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert "def method2" in content + # Check 1-space indentation + assert " def method2(self):" in content def test_py_region_wrap(temp_py_file): - err = py_struct_tools.py_region_wrap(temp_py_file, 6, 8, "MyRegion") - assert err == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert "#region: MyRegion" in content - assert "#endregion: MyRegion" in content + err = py_struct_tools.py_region_wrap(temp_py_file, 6, 8, "MyRegion") + assert err == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert "#region: MyRegion" in content + assert "#endregion: MyRegion" in content def test_mcp_dispatch_integration(temp_py_file): - # Mock allowlist - mcp_client.configure([{"path": temp_py_file}]) + # Mock allowlist + mcp_client.configure([{"path": temp_py_file}]) - # Test py_remove_def - result = mcp_client.dispatch("py_remove_def", {"path": temp_py_file, "name": "top_func"}) - assert result == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert "def top_func" not in content + # Test py_remove_def + result = mcp_client.dispatch("py_remove_def", {"path": temp_py_file, "name": "top_func"}) + assert result == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert "def top_func" not in content - # Test py_add_def (module level top) - result = mcp_client.dispatch("py_add_def", { - "path": temp_py_file, - "name": "", - "new_content": "def head_func():\n print('head')", - "anchor_type": "top" - }) - assert result == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert content.startswith("def head_func") + # Test py_add_def (module level top) + result = mcp_client.dispatch("py_add_def", { + "path": temp_py_file, + "name": "", + "new_content": "def head_func():\n print('head')", + "anchor_type": "top" + }) + assert result == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert content.startswith("def head_func") - # Test py_add_def (class bottom) - result = mcp_client.dispatch("py_add_def", { - "path": temp_py_file, - "name": "MyClass", - "new_content": "def tail_method(self):\n print('tail')", - "anchor_type": "bottom" - }) - assert result == "" - with open(temp_py_file, 'r') as f: - content = f.read() - assert "def tail_method" in content - assert " def tail_method(self):" in content # Check indent + # Test py_add_def (class bottom) + result = mcp_client.dispatch("py_add_def", { + "path": temp_py_file, + "name": "MyClass", + "new_content": "def tail_method(self):\n print('tail')", + "anchor_type": "bottom" + }) + assert result == "" + with open(temp_py_file, 'r') as f: + content = f.read() + assert "def tail_method" in content + assert " def tail_method(self):" in content # Check indent - # Test py_move_def (cross-file simulated with same file) - # We move method1 to after tail_method - result = mcp_client.dispatch("py_move_def", { - "src_path": temp_py_file, - "dest_path": temp_py_file, - "name": "MyClass.method1", - "dest_name": "MyClass", - "anchor_type": "after", - "anchor_symbol": "tail_method" - }) - assert result == "" - with open(temp_py_file, 'r') as f: - content = f.read() - # method1 should now be AFTER tail_method - assert content.find("def method1") > content.find("def tail_method") + # Test py_move_def (cross-file simulated with same file) + # We move method1 to after tail_method + result = mcp_client.dispatch("py_move_def", { + "src_path": temp_py_file, + "dest_path": temp_py_file, + "name": "MyClass.method1", + "dest_name": "MyClass", + "anchor_type": "after", + "anchor_symbol": "tail_method" + }) + assert result == "" + with open(temp_py_file, 'r') as f: + content = f.read() + # method1 should now be AFTER tail_method + assert content.find("def method1") > content.find("def tail_method") def test_mcp_dispatch_errors(temp_py_file): - mcp_client.configure([{"path": temp_py_file}]) + mcp_client.configure([{"path": temp_py_file}]) - # Non-existent symbol - result = mcp_client.dispatch("py_remove_def", {"path": temp_py_file, "name": "NoSuchSymbol"}) - assert "ERROR" in result or "not found" in result + # Non-existent symbol + result = mcp_client.dispatch("py_remove_def", {"path": temp_py_file, "name": "NoSuchSymbol"}) + assert "ERROR" in result or "not found" in result - # Denied path - result = mcp_client.dispatch("py_remove_def", {"path": "C:/windows/system32/cmd.exe", "name": "foo"}) - assert "ACCESS DENIED" in result + # Denied path + result = mcp_client.dispatch("py_remove_def", {"path": "C:/windows/system32/cmd.exe", "name": "foo"}) + assert "ACCESS DENIED" in result diff --git a/tests/test_rag_gui_presence.py b/tests/test_rag_gui_presence.py index 0e2ebe78..4b6f94e3 100644 --- a/tests/test_rag_gui_presence.py +++ b/tests/test_rag_gui_presence.py @@ -1,15 +1,15 @@ - import pytest +import src.gui_2 as gui_2 from src.gui_2 import App def test_rag_panel_exists(): - """Verify that _render_rag_panel has been added to the App class.""" - assert hasattr(App, '_render_rag_panel') - assert callable(App._render_rag_panel) + """Verify that render_rag_panel exists as a module-level function.""" + assert hasattr(gui_2, 'render_rag_panel'), "gui_2 module must have render_rag_panel function" + assert callable(gui_2.render_rag_panel) def test_rag_panel_integration(): - """Verify that _render_ai_settings_hub calls _render_rag_panel by inspecting source or via mock.""" + """Verify that render_ai_settings_hub calls render_rag_panel by inspecting source.""" import inspect - source = inspect.getsource(App._render_ai_settings_hub) - assert "self._render_rag_panel()" in source - assert "imgui.collapsing_header(\"RAG Settings\")" in source \ No newline at end of file + source = inspect.getsource(gui_2.render_ai_settings_hub) + assert "render_rag_panel(app)" in source + assert "imgui.collapsing_header(\"RAG Settings\")" in source diff --git a/tests/test_selectable_ui.py b/tests/test_selectable_ui.py index 4d507b5c..1aea95f4 100644 --- a/tests/test_selectable_ui.py +++ b/tests/test_selectable_ui.py @@ -3,47 +3,29 @@ import time import os import sys -# Ensure project root is in path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from src.api_hook_client import ApiHookClient -from src.gui_2 import App +import src.gui_2 as gui_2 @pytest.mark.integration def test_selectable_label_stability(live_gui) -> None: """ - - Verifies that the application starts correctly with --enable-test-hooks and that the selectable label infrastructure is present and stable. """ client = ApiHookClient() assert client.wait_for_server(timeout=20), "Hook server failed to start" - # 1. Check initial state via diagnostics: is_viewing_prior_session should be False diag = client.get_gui_diagnostics() - # Based on src/api_hooks.py: result["prior"] = _get_app_attr(app, "is_viewing_prior_session", False) assert diag.get("prior") is False, "Initial state should not be viewing prior session" - # 2. Verify _render_selectable_label exists in the App class - # This satisfies the requirement to check if it exists in the App class. - assert hasattr(App, '_render_selectable_label'), "App class must have _render_selectable_label method" + assert hasattr(gui_2, 'render_selectable_label'), "gui_2 module must have render_selectable_label function" - # 3. Check performance to ensure stability - perf = client.get_performance() - metrics = perf.get("performance", {}) - # We check if FPS is reported; in some CI environments it might be low but should be > 0 if rendering - assert "fps" in metrics, "Performance metrics should include FPS" - - # 4. Basic smoke test: set and get a value to ensure GUI thread is responsive - # ai_response is a known field that is often rendered using selectable labels in various contexts client.set_value("ai_response", "Test selectable text stability") - # Give it a few frames to process the task time.sleep(1) val = client.get_value("ai_response") assert val == "Test selectable text stability", f"Expected 'Test selectable text stability', got '{val}'" - # 5. Verify prior session indicator specifically via the gettable field - # prior_session_indicator is mapped to AppController.is_viewing_prior_session prior_val = client.get_value("prior_session_indicator") - assert prior_val is False, "prior_session_indicator field should be False initially" \ No newline at end of file + assert prior_val is False, "prior_session_indicator field should be False initially" diff --git a/tests/test_thinking_gui.py b/tests/test_thinking_gui.py index d13efea3..ecb27a56 100644 --- a/tests/test_thinking_gui.py +++ b/tests/test_thinking_gui.py @@ -1,14 +1,11 @@ import pytest - +import src.gui_2 as gui_2 def test_render_thinking_trace_helper_exists(): - from src.gui_2 import App - - assert hasattr(App, "_render_thinking_trace"), ( - "_render_thinking_trace helper should exist in App class" + assert hasattr(gui_2, 'render_thinking_trace'), ( + "gui_2 module must have render_thinking_trace function" ) - def test_discussion_entry_with_thinking_segments(): entry = { "role": "AI", diff --git a/tests/test_token_viz.py b/tests/test_token_viz.py index 3f0c88dc..1f0af0b2 100644 --- a/tests/test_token_viz.py +++ b/tests/test_token_viz.py @@ -56,8 +56,9 @@ def test_app_last_stable_md_initialized_empty(app_instance: Any) -> None: assert app_instance.controller._last_stable_md == '' def test_app_has_render_token_budget_panel(app_instance: Any) -> None: - """App must have _render_token_budget_panel method.""" - assert hasattr(app_instance, "_render_token_budget_panel") + """App must have render_token_budget_panel function in gui_2 module.""" + import src.gui_2 as gui_2 + assert hasattr(gui_2, 'render_token_budget_panel'), "gui_2 module must have render_token_budget_panel function" def test_would_trim_boundary_exact() -> None: """Exact limit should trigger would_trim (cur >= lim)."""